mirror of
https://github.com/sigp/lighthouse.git
synced 2026-06-29 10:54:24 +00:00
Directory Restructure (#1163)
* Move tests -> testing * Directory restructure * Update Cargo.toml during restructure * Update Makefile during restructure * Fix arbitrary path
This commit is contained in:
3
common/README.md
Normal file
3
common/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# eth2
|
||||
|
||||
Common crates containing eth2-specific logic.
|
||||
15
common/clap_utils/Cargo.toml
Normal file
15
common/clap_utils/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "clap_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]
|
||||
clap = "2.33.0"
|
||||
hex = "0.4.2"
|
||||
dirs = "2.0.2"
|
||||
types = { path = "../../consensus/types" }
|
||||
eth2_testnet_config = { path = "../eth2_testnet_config" }
|
||||
eth2_ssz = "0.1.2"
|
||||
112
common/clap_utils/src/lib.rs
Normal file
112
common/clap_utils/src/lib.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
//! A helper library for parsing values from `clap::ArgMatches`.
|
||||
|
||||
use clap::ArgMatches;
|
||||
use eth2_testnet_config::Eth2TestnetConfig;
|
||||
use hex;
|
||||
use ssz::Decode;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use types::EthSpec;
|
||||
|
||||
pub const BAD_TESTNET_DIR_MESSAGE: &str = "The hard-coded testnet directory was invalid. \
|
||||
This happens when Lighthouse is migrating between spec versions \
|
||||
or when there is no default public network to connect to. \
|
||||
During these times you must specify a --testnet-dir.";
|
||||
|
||||
/// Attempts to load the testnet dir at the path if `name` is in `matches`, returning an error if
|
||||
/// the path cannot be found or the testnet dir is invalid.
|
||||
///
|
||||
/// If `name` is not in `matches`, attempts to return the "hard coded" testnet dir.
|
||||
pub fn parse_testnet_dir_with_hardcoded_default<E: EthSpec>(
|
||||
matches: &ArgMatches,
|
||||
name: &'static str,
|
||||
) -> Result<Eth2TestnetConfig<E>, String> {
|
||||
if let Some(path) = parse_optional::<PathBuf>(matches, name)? {
|
||||
Eth2TestnetConfig::load(path.clone())
|
||||
.map_err(|e| format!("Unable to open testnet dir at {:?}: {}", path, e))
|
||||
} else {
|
||||
Eth2TestnetConfig::hard_coded()
|
||||
.map_err(|e| format!("{} Error : {}", BAD_TESTNET_DIR_MESSAGE, e))
|
||||
}
|
||||
}
|
||||
|
||||
/// If `name` is in `matches`, parses the value as a path. Otherwise, attempts to find the user's
|
||||
/// home directory and appends `default` to it.
|
||||
pub fn parse_path_with_default_in_home_dir(
|
||||
matches: &ArgMatches,
|
||||
name: &'static str,
|
||||
default: PathBuf,
|
||||
) -> Result<PathBuf, String> {
|
||||
matches
|
||||
.value_of(name)
|
||||
.map(|dir| {
|
||||
dir.parse::<PathBuf>()
|
||||
.map_err(|e| format!("Unable to parse {}: {}", name, e))
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
dirs::home_dir()
|
||||
.map(|home| home.join(default))
|
||||
.ok_or_else(|| format!("Unable to locate home directory. Try specifying {}", name))
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the value of `name` or an error if it is not in `matches` or does not parse
|
||||
/// successfully using `std::string::FromStr`.
|
||||
pub fn parse_required<T>(matches: &ArgMatches, name: &'static str) -> Result<T, String>
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
parse_optional(matches, name)?.ok_or_else(|| format!("{} not specified", name))
|
||||
}
|
||||
|
||||
/// Returns the value of `name` (if present) or an error if it does not parse successfully using
|
||||
/// `std::string::FromStr`.
|
||||
pub fn parse_optional<T>(matches: &ArgMatches, name: &'static str) -> Result<Option<T>, String>
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: std::fmt::Display,
|
||||
{
|
||||
matches
|
||||
.value_of(name)
|
||||
.map(|val| {
|
||||
val.parse()
|
||||
.map_err(|e| format!("Unable to parse {}: {}", name, e))
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
/// Returns the value of `name` or an error if it is not in `matches` or does not parse
|
||||
/// successfully using `ssz::Decode`.
|
||||
///
|
||||
/// Expects the value of `name` to be 0x-prefixed ASCII-hex.
|
||||
pub fn parse_ssz_required<T: Decode>(
|
||||
matches: &ArgMatches,
|
||||
name: &'static str,
|
||||
) -> Result<T, String> {
|
||||
parse_ssz_optional(matches, name)?.ok_or_else(|| format!("{} not specified", name))
|
||||
}
|
||||
|
||||
/// Returns the value of `name` (if present) or an error if it does not parse successfully using
|
||||
/// `ssz::Decode`.
|
||||
///
|
||||
/// Expects the value of `name` (if any) to be 0x-prefixed ASCII-hex.
|
||||
pub fn parse_ssz_optional<T: Decode>(
|
||||
matches: &ArgMatches,
|
||||
name: &'static str,
|
||||
) -> Result<Option<T>, String> {
|
||||
matches
|
||||
.value_of(name)
|
||||
.map(|val| {
|
||||
if val.starts_with("0x") {
|
||||
let vec = hex::decode(&val[2..])
|
||||
.map_err(|e| format!("Unable to parse {} as hex: {:?}", name, e))?;
|
||||
|
||||
T::from_ssz_bytes(&vec)
|
||||
.map_err(|e| format!("Unable to parse {} as SSZ: {:?}", name, e))
|
||||
} else {
|
||||
Err(format!("Unable to parse {}, must have 0x prefix", name))
|
||||
}
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
10
common/compare_fields/Cargo.toml
Normal file
10
common/compare_fields/Cargo.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "compare_fields"
|
||||
version = "0.2.0"
|
||||
authors = ["Paul Hauner <paul@paulhauner.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dev-dependencies]
|
||||
compare_fields_derive = { path = "../compare_fields_derive" }
|
||||
|
||||
[dependencies]
|
||||
179
common/compare_fields/src/lib.rs
Normal file
179
common/compare_fields/src/lib.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
//! Provides field-by-field comparisons for structs and vecs.
|
||||
//!
|
||||
//! Returns comparisons as data, without making assumptions about the desired equality (e.g.,
|
||||
//! does not `panic!` on inequality).
|
||||
//!
|
||||
//! Note: `compare_fields_derive` requires `PartialEq` and `Debug` implementations.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use compare_fields::{CompareFields, Comparison, FieldComparison};
|
||||
//! use compare_fields_derive::CompareFields;
|
||||
//!
|
||||
//! #[derive(PartialEq, Debug, CompareFields)]
|
||||
//! pub struct Bar {
|
||||
//! a: u64,
|
||||
//! b: u16,
|
||||
//! #[compare_fields(as_slice)]
|
||||
//! c: Vec<Foo>
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Clone, PartialEq, Debug, CompareFields)]
|
||||
//! pub struct Foo {
|
||||
//! d: String
|
||||
//! }
|
||||
//!
|
||||
//! let cat = Foo {d: "cat".to_string()};
|
||||
//! let dog = Foo {d: "dog".to_string()};
|
||||
//! let chicken = Foo {d: "chicken".to_string()};
|
||||
//!
|
||||
//! let mut bar_a = Bar {
|
||||
//! a: 42,
|
||||
//! b: 12,
|
||||
//! c: vec![ cat.clone(), dog.clone() ],
|
||||
//! };
|
||||
//!
|
||||
//! let mut bar_b = Bar {
|
||||
//! a: 42,
|
||||
//! b: 99,
|
||||
//! c: vec![ chicken.clone(), dog.clone()]
|
||||
//! };
|
||||
//!
|
||||
//! let cat_dog = Comparison::Child(FieldComparison {
|
||||
//! field_name: "d".to_string(),
|
||||
//! equal: false,
|
||||
//! a: "\"cat\"".to_string(),
|
||||
//! b: "\"dog\"".to_string(),
|
||||
//! });
|
||||
//! assert_eq!(cat.compare_fields(&dog), vec![cat_dog]);
|
||||
//!
|
||||
//! let bar_a_b = vec![
|
||||
//! Comparison::Child(FieldComparison {
|
||||
//! field_name: "a".to_string(),
|
||||
//! equal: true,
|
||||
//! a: "42".to_string(),
|
||||
//! b: "42".to_string(),
|
||||
//! }),
|
||||
//! Comparison::Child(FieldComparison {
|
||||
//! field_name: "b".to_string(),
|
||||
//! equal: false,
|
||||
//! a: "12".to_string(),
|
||||
//! b: "99".to_string(),
|
||||
//! }),
|
||||
//! Comparison::Parent{
|
||||
//! field_name: "c".to_string(),
|
||||
//! equal: false,
|
||||
//! children: vec![
|
||||
//! FieldComparison {
|
||||
//! field_name: "0".to_string(),
|
||||
//! equal: false,
|
||||
//! a: "Some(Foo { d: \"cat\" })".to_string(),
|
||||
//! b: "Some(Foo { d: \"chicken\" })".to_string(),
|
||||
//! },
|
||||
//! FieldComparison {
|
||||
//! field_name: "1".to_string(),
|
||||
//! equal: true,
|
||||
//! a: "Some(Foo { d: \"dog\" })".to_string(),
|
||||
//! b: "Some(Foo { d: \"dog\" })".to_string(),
|
||||
//! }
|
||||
//! ]
|
||||
//! }
|
||||
//! ];
|
||||
//! assert_eq!(bar_a.compare_fields(&bar_b), bar_a_b);
|
||||
//!
|
||||
//!
|
||||
//!
|
||||
//! // TODO:
|
||||
//! ```
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Comparison {
|
||||
Child(FieldComparison),
|
||||
Parent {
|
||||
field_name: String,
|
||||
equal: bool,
|
||||
children: Vec<FieldComparison>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Comparison {
|
||||
pub fn child<T: Debug + PartialEq<T>>(field_name: String, a: &T, b: &T) -> Self {
|
||||
Comparison::Child(FieldComparison::new(field_name, a, b))
|
||||
}
|
||||
|
||||
pub fn parent(field_name: String, equal: bool, children: Vec<FieldComparison>) -> Self {
|
||||
Comparison::Parent {
|
||||
field_name,
|
||||
equal,
|
||||
children,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_slice<T: Debug + PartialEq<T>>(field_name: String, a: &[T], b: &[T]) -> Self {
|
||||
let mut children = vec![];
|
||||
|
||||
for i in 0..std::cmp::max(a.len(), b.len()) {
|
||||
children.push(FieldComparison::new(
|
||||
format!("{:}", i),
|
||||
&a.get(i),
|
||||
&b.get(i),
|
||||
));
|
||||
}
|
||||
|
||||
Self::parent(field_name, a == b, children)
|
||||
}
|
||||
|
||||
pub fn retain_children<F>(&mut self, f: F)
|
||||
where
|
||||
F: FnMut(&FieldComparison) -> bool,
|
||||
{
|
||||
match self {
|
||||
Comparison::Child(_) => (),
|
||||
Comparison::Parent { children, .. } => children.retain(f),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn equal(&self) -> bool {
|
||||
match self {
|
||||
Comparison::Child(fc) => fc.equal,
|
||||
Comparison::Parent { equal, .. } => *equal,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not_equal(&self) -> bool {
|
||||
!self.equal()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct FieldComparison {
|
||||
pub field_name: String,
|
||||
pub equal: bool,
|
||||
pub a: String,
|
||||
pub b: String,
|
||||
}
|
||||
|
||||
pub trait CompareFields {
|
||||
fn compare_fields(&self, b: &Self) -> Vec<Comparison>;
|
||||
}
|
||||
|
||||
impl FieldComparison {
|
||||
pub fn new<T: Debug + PartialEq<T>>(field_name: String, a: &T, b: &T) -> Self {
|
||||
Self {
|
||||
field_name,
|
||||
equal: a == b,
|
||||
a: format!("{:?}", a),
|
||||
b: format!("{:?}", b),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn equal(&self) -> bool {
|
||||
self.equal
|
||||
}
|
||||
|
||||
pub fn not_equal(&self) -> bool {
|
||||
!self.equal()
|
||||
}
|
||||
}
|
||||
12
common/compare_fields_derive/Cargo.toml
Normal file
12
common/compare_fields_derive/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "compare_fields_derive"
|
||||
version = "0.2.0"
|
||||
authors = ["Paul Hauner <paul@paulhauner.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "1.0.18"
|
||||
quote = "1.0.4"
|
||||
75
common/compare_fields_derive/src/lib.rs
Normal file
75
common/compare_fields_derive/src/lib.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
#![recursion_limit = "256"]
|
||||
extern crate proc_macro;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
|
||||
fn is_slice(field: &syn::Field) -> bool {
|
||||
field.attrs.iter().any(|attr| {
|
||||
attr.path.is_ident("compare_fields")
|
||||
&& attr.tokens.to_string().replace(" ", "") == "(as_slice)"
|
||||
})
|
||||
}
|
||||
|
||||
#[proc_macro_derive(CompareFields, attributes(compare_fields))]
|
||||
pub fn compare_fields_derive(input: TokenStream) -> TokenStream {
|
||||
let item = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
let name = &item.ident;
|
||||
let (impl_generics, ty_generics, where_clause) = &item.generics.split_for_impl();
|
||||
|
||||
let struct_data = match &item.data {
|
||||
syn::Data::Struct(s) => s,
|
||||
_ => panic!("compare_fields_derive only supports structs."),
|
||||
};
|
||||
|
||||
let mut quotes = vec![];
|
||||
|
||||
for field in struct_data.fields.iter() {
|
||||
let ident_a = match &field.ident {
|
||||
Some(ref ident) => ident,
|
||||
_ => panic!("compare_fields_derive only supports named struct fields."),
|
||||
};
|
||||
|
||||
let field_name = format!("{:}", ident_a);
|
||||
let ident_b = ident_a.clone();
|
||||
|
||||
let quote = if is_slice(field) {
|
||||
quote! {
|
||||
comparisons.push(compare_fields::Comparison::from_slice(
|
||||
#field_name.to_string(),
|
||||
&self.#ident_a,
|
||||
&b.#ident_b)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
comparisons.push(
|
||||
compare_fields::Comparison::child(
|
||||
#field_name.to_string(),
|
||||
&self.#ident_a,
|
||||
&b.#ident_b
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
quotes.push(quote);
|
||||
}
|
||||
|
||||
let output = quote! {
|
||||
impl #impl_generics compare_fields::CompareFields for #name #ty_generics #where_clause {
|
||||
fn compare_fields(&self, b: &Self) -> Vec<compare_fields::Comparison> {
|
||||
let mut comparisons = vec![];
|
||||
|
||||
#(
|
||||
#quotes
|
||||
)*
|
||||
|
||||
comparisons
|
||||
}
|
||||
}
|
||||
};
|
||||
output.into()
|
||||
}
|
||||
1
common/deposit_contract/.gitignore
vendored
Normal file
1
common/deposit_contract/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
contracts/
|
||||
17
common/deposit_contract/Cargo.toml
Normal file
17
common/deposit_contract/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "deposit_contract"
|
||||
version = "0.2.0"
|
||||
authors = ["Paul Hauner <paul@paulhauner.com>"]
|
||||
edition = "2018"
|
||||
|
||||
build = "build.rs"
|
||||
|
||||
[build-dependencies]
|
||||
reqwest = { version = "0.10.4", features = ["blocking", "json"] }
|
||||
serde_json = "1.0.52"
|
||||
|
||||
[dependencies]
|
||||
types = { path = "../../consensus/types"}
|
||||
eth2_ssz = "0.1.2"
|
||||
tree_hash = "0.1.0"
|
||||
ethabi = "12.0.0"
|
||||
110
common/deposit_contract/build.rs
Normal file
110
common/deposit_contract/build.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
//! Downloads the ABI and bytecode for the deposit contract from the ethereum spec repository and
|
||||
//! stores them in a `contract/` directory in the crate root.
|
||||
//!
|
||||
//! These files are required for some `include_bytes` calls used in this crate.
|
||||
|
||||
use serde_json::Value;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const TAG: &str = "v0.11.1";
|
||||
// NOTE: the version of the unsafe contract lags the main tag, but the v0.9.2.1 code is compatible
|
||||
// with the unmodified v0.11.1 contract
|
||||
const UNSAFE_TAG: &str = "v0.9.2.1";
|
||||
|
||||
fn spec_url() -> String {
|
||||
format!("https://raw.githubusercontent.com/ethereum/eth2.0-specs/{}/deposit_contract/contracts/validator_registration.json", TAG)
|
||||
}
|
||||
fn testnet_url() -> String {
|
||||
format!("https://raw.githubusercontent.com/sigp/unsafe-eth2-deposit-contract/{}/unsafe_validator_registration.json", UNSAFE_TAG)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
match get_all_contracts() {
|
||||
Ok(()) => (),
|
||||
Err(e) => panic!(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to download the deposit contract ABI from github if a local copy is not already
|
||||
/// present.
|
||||
pub fn get_all_contracts() -> Result<(), String> {
|
||||
download_deposit_contract(
|
||||
&spec_url(),
|
||||
"validator_registration.json",
|
||||
"validator_registration.bytecode",
|
||||
)?;
|
||||
download_deposit_contract(
|
||||
&testnet_url(),
|
||||
"testnet_validator_registration.json",
|
||||
"testnet_validator_registration.bytecode",
|
||||
)
|
||||
}
|
||||
|
||||
/// Attempts to download the deposit contract ABI from github if a local copy is not already
|
||||
/// present.
|
||||
pub fn download_deposit_contract(
|
||||
url: &str,
|
||||
abi_file: &str,
|
||||
bytecode_file: &str,
|
||||
) -> Result<(), String> {
|
||||
let abi_file = abi_dir().join(format!("{}_{}", TAG, abi_file));
|
||||
let bytecode_file = abi_dir().join(format!("{}_{}", TAG, bytecode_file));
|
||||
|
||||
if abi_file.exists() {
|
||||
// Nothing to do.
|
||||
} else {
|
||||
match reqwest::blocking::get(url) {
|
||||
Ok(response) => {
|
||||
let mut abi_file = File::create(abi_file)
|
||||
.map_err(|e| format!("Failed to create local abi file: {:?}", e))?;
|
||||
let mut bytecode_file = File::create(bytecode_file)
|
||||
.map_err(|e| format!("Failed to create local bytecode file: {:?}", e))?;
|
||||
|
||||
let contract: Value = response
|
||||
.json()
|
||||
.map_err(|e| format!("Respsonse is not a valid json {:?}", e))?;
|
||||
|
||||
let abi = contract
|
||||
.get("abi")
|
||||
.ok_or(format!("Response does not contain key: abi"))?
|
||||
.to_string();
|
||||
abi_file
|
||||
.write(abi.as_bytes())
|
||||
.map_err(|e| format!("Failed to write http response to abi file: {:?}", e))?;
|
||||
|
||||
let bytecode = contract
|
||||
.get("bytecode")
|
||||
.ok_or(format!("Response does not contain key: bytecode"))?
|
||||
.to_string();
|
||||
bytecode_file.write(bytecode.as_bytes()).map_err(|e| {
|
||||
format!("Failed to write http response to bytecode file: {:?}", e)
|
||||
})?;
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(format!(
|
||||
"No abi file found. Failed to download from github: {:?}",
|
||||
e
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the directory that will be used to store the deposit contract ABI.
|
||||
fn abi_dir() -> PathBuf {
|
||||
let base = env::var("CARGO_MANIFEST_DIR")
|
||||
.expect("should know manifest dir")
|
||||
.parse::<PathBuf>()
|
||||
.expect("should parse manifest dir as path")
|
||||
.join("contracts");
|
||||
|
||||
std::fs::create_dir_all(base.clone())
|
||||
.expect("should be able to create abi directory in manifest");
|
||||
|
||||
base
|
||||
}
|
||||
131
common/deposit_contract/src/lib.rs
Normal file
131
common/deposit_contract/src/lib.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use ethabi::{Contract, Token};
|
||||
use ssz::{Decode, DecodeError as SszDecodeError, Encode};
|
||||
use tree_hash::TreeHash;
|
||||
use types::{DepositData, Hash256, PublicKeyBytes, SignatureBytes};
|
||||
|
||||
pub use ethabi::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DecodeError {
|
||||
EthabiError(ethabi::Error),
|
||||
SszDecodeError(SszDecodeError),
|
||||
MissingField,
|
||||
UnableToGetBytes,
|
||||
MissingToken,
|
||||
InadequateBytes,
|
||||
}
|
||||
|
||||
impl From<ethabi::Error> for DecodeError {
|
||||
fn from(e: ethabi::Error) -> DecodeError {
|
||||
DecodeError::EthabiError(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub const CONTRACT_DEPLOY_GAS: usize = 4_000_000;
|
||||
pub const DEPOSIT_GAS: usize = 400_000;
|
||||
pub const ABI: &[u8] = include_bytes!("../contracts/v0.11.1_validator_registration.json");
|
||||
pub const BYTECODE: &[u8] = include_bytes!("../contracts/v0.11.1_validator_registration.bytecode");
|
||||
pub const DEPOSIT_DATA_LEN: usize = 420; // lol
|
||||
|
||||
pub mod testnet {
|
||||
pub const ABI: &[u8] =
|
||||
include_bytes!("../contracts/v0.11.1_testnet_validator_registration.json");
|
||||
pub const BYTECODE: &[u8] =
|
||||
include_bytes!("../contracts/v0.11.1_testnet_validator_registration.bytecode");
|
||||
}
|
||||
|
||||
pub fn encode_eth1_tx_data(deposit_data: &DepositData) -> Result<Vec<u8>, Error> {
|
||||
let params = vec![
|
||||
Token::Bytes(deposit_data.pubkey.as_ssz_bytes()),
|
||||
Token::Bytes(deposit_data.withdrawal_credentials.as_ssz_bytes()),
|
||||
Token::Bytes(deposit_data.signature.as_ssz_bytes()),
|
||||
Token::FixedBytes(deposit_data.tree_hash_root().as_ssz_bytes()),
|
||||
];
|
||||
|
||||
// Here we make an assumption that the `crate::testnet::ABI` has a superset of the features of
|
||||
// the crate::ABI`.
|
||||
let abi = Contract::load(ABI)?;
|
||||
let function = abi.function("deposit")?;
|
||||
function.encode_input(¶ms)
|
||||
}
|
||||
|
||||
pub fn decode_eth1_tx_data(
|
||||
bytes: &[u8],
|
||||
amount: u64,
|
||||
) -> Result<(DepositData, Hash256), DecodeError> {
|
||||
let abi = Contract::load(ABI)?;
|
||||
let function = abi.function("deposit")?;
|
||||
let mut tokens =
|
||||
function.decode_input(bytes.get(4..).ok_or_else(|| DecodeError::InadequateBytes)?)?;
|
||||
|
||||
macro_rules! decode_token {
|
||||
($type: ty, $to_fn: ident) => {
|
||||
<$type>::from_ssz_bytes(
|
||||
&tokens
|
||||
.pop()
|
||||
.ok_or_else(|| DecodeError::MissingToken)?
|
||||
.$to_fn()
|
||||
.ok_or_else(|| DecodeError::UnableToGetBytes)?,
|
||||
)
|
||||
.map_err(DecodeError::SszDecodeError)?
|
||||
};
|
||||
};
|
||||
|
||||
let root = decode_token!(Hash256, to_fixed_bytes);
|
||||
|
||||
let deposit_data = DepositData {
|
||||
amount,
|
||||
signature: decode_token!(SignatureBytes, to_bytes),
|
||||
withdrawal_credentials: decode_token!(Hash256, to_bytes),
|
||||
pubkey: decode_token!(PublicKeyBytes, to_bytes),
|
||||
};
|
||||
|
||||
Ok((deposit_data, root))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use types::{
|
||||
test_utils::generate_deterministic_keypair, ChainSpec, EthSpec, Hash256, Keypair,
|
||||
MinimalEthSpec, Signature,
|
||||
};
|
||||
|
||||
type E = MinimalEthSpec;
|
||||
|
||||
fn get_deposit(keypair: Keypair, spec: &ChainSpec) -> DepositData {
|
||||
let mut deposit_data = DepositData {
|
||||
pubkey: keypair.pk.into(),
|
||||
withdrawal_credentials: Hash256::from_slice(&[42; 32]),
|
||||
amount: u64::max_value(),
|
||||
signature: Signature::empty_signature().into(),
|
||||
};
|
||||
deposit_data.signature = deposit_data.create_signature(&keypair.sk, spec);
|
||||
deposit_data
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
let spec = &E::default_spec();
|
||||
|
||||
let keypair = generate_deterministic_keypair(42);
|
||||
let original = get_deposit(keypair, spec);
|
||||
|
||||
let data = encode_eth1_tx_data(&original).expect("should produce tx data");
|
||||
|
||||
assert_eq!(
|
||||
data.len(),
|
||||
DEPOSIT_DATA_LEN,
|
||||
"bytes should be correct length"
|
||||
);
|
||||
|
||||
let (decoded, root) = decode_eth1_tx_data(&data, original.amount).expect("should decode");
|
||||
|
||||
assert_eq!(decoded, original, "decoded should match original");
|
||||
assert_eq!(
|
||||
root,
|
||||
original.tree_hash_root(),
|
||||
"decode root should match original root"
|
||||
);
|
||||
}
|
||||
}
|
||||
11
common/eth2_config/Cargo.toml
Normal file
11
common/eth2_config/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "eth2_config"
|
||||
version = "0.2.0"
|
||||
authors = ["Paul Hauner <paul@paulhauner.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0.110"
|
||||
serde_derive = "1.0.110"
|
||||
toml = "0.5.6"
|
||||
types = { path = "../../consensus/types" }
|
||||
54
common/eth2_config/src/lib.rs
Normal file
54
common/eth2_config/src/lib.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use types::ChainSpec;
|
||||
|
||||
/// The core configuration of a Lighthouse beacon node.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct Eth2Config {
|
||||
pub spec_constants: String,
|
||||
pub spec: ChainSpec,
|
||||
}
|
||||
|
||||
impl Default for Eth2Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
spec_constants: "minimal".to_string(),
|
||||
spec: ChainSpec::minimal(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eth2Config {
|
||||
pub fn mainnet() -> Self {
|
||||
Self {
|
||||
spec_constants: "mainnet".to_string(),
|
||||
spec: ChainSpec::mainnet(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn minimal() -> Self {
|
||||
Self {
|
||||
spec_constants: "minimal".to_string(),
|
||||
spec: ChainSpec::minimal(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn interop() -> Self {
|
||||
Self {
|
||||
spec_constants: "interop".to_string(),
|
||||
spec: ChainSpec::interop(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use toml;
|
||||
|
||||
#[test]
|
||||
fn serde_serialize() {
|
||||
let _ =
|
||||
toml::to_string(&Eth2Config::default()).expect("Should serde encode default config");
|
||||
}
|
||||
}
|
||||
20
common/eth2_interop_keypairs/Cargo.toml
Normal file
20
common/eth2_interop_keypairs/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "eth2_interop_keypairs"
|
||||
version = "0.2.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]
|
||||
lazy_static = "1.4.0"
|
||||
num-bigint = "0.2.6"
|
||||
eth2_hashing = "0.1.0"
|
||||
hex = "0.4.2"
|
||||
milagro_bls = { git = "https://github.com/sigp/milagro_bls", tag = "v1.0.1" }
|
||||
serde_yaml = "0.8.11"
|
||||
serde = "1.0.110"
|
||||
serde_derive = "1.0.110"
|
||||
|
||||
[dev-dependencies]
|
||||
base64 = "0.12.1"
|
||||
20
common/eth2_interop_keypairs/specs/keygen_10_validators.yaml
Normal file
20
common/eth2_interop_keypairs/specs/keygen_10_validators.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
- {privkey: '0x25295f0d1d592a90b333e26e85149708208e9f8e8bc18f6c77bd62f8ad7a6866',
|
||||
pubkey: '0xa99a76ed7796f7be22d5b7e85deeb7c5677e88e511e0b337618f8c4eb61349b4bf2d153f649f7b53359fe8b94a38e44c'}
|
||||
- {privkey: '0x51d0b65185db6989ab0b560d6deed19c7ead0e24b9b6372cbecb1f26bdfad000',
|
||||
pubkey: '0xb89bebc699769726a318c8e9971bd3171297c61aea4a6578a7a4f94b547dcba5bac16a89108b6b6a1fe3695d1a874a0b'}
|
||||
- {privkey: '0x315ed405fafe339603932eebe8dbfd650ce5dafa561f6928664c75db85f97857',
|
||||
pubkey: '0xa3a32b0f8b4ddb83f1a0a853d81dd725dfe577d4f4c3db8ece52ce2b026eca84815c1a7e8e92a4de3d755733bf7e4a9b'}
|
||||
- {privkey: '0x25b1166a43c109cb330af8945d364722757c65ed2bfed5444b5a2f057f82d391',
|
||||
pubkey: '0x88c141df77cd9d8d7a71a75c826c41a9c9f03c6ee1b180f3e7852f6a280099ded351b58d66e653af8e42816a4d8f532e'}
|
||||
- {privkey: '0x3f5615898238c4c4f906b507ee917e9ea1bb69b93f1dbd11a34d229c3b06784b',
|
||||
pubkey: '0x81283b7a20e1ca460ebd9bbd77005d557370cabb1f9a44f530c4c4c66230f675f8df8b4c2818851aa7d77a80ca5a4a5e'}
|
||||
- {privkey: '0x055794614bc85ed5436c1f5cab586aab6ca84835788621091f4f3b813761e7a8',
|
||||
pubkey: '0xab0bdda0f85f842f431beaccf1250bf1fd7ba51b4100fd64364b6401fda85bb0069b3e715b58819684e7fc0b10a72a34'}
|
||||
- {privkey: '0x1023c68852075965e0f7352dee3f76a84a83e7582c181c10179936c6d6348893',
|
||||
pubkey: '0x9977f1c8b731a8d5558146bfb86caea26434f3c5878b589bf280a42c9159e700e9df0e4086296c20b011d2e78c27d373'}
|
||||
- {privkey: '0x3a941600dc41e5d20e818473b817a28507c23cdfdb4b659c15461ee5c71e41f5',
|
||||
pubkey: '0xa8d4c7c27795a725961317ef5953a7032ed6d83739db8b0e8a72353d1b8b4439427f7efa2c89caa03cc9f28f8cbab8ac'}
|
||||
- {privkey: '0x066e3bdc0415530e5c7fed6382d5c822c192b620203cf669903e1810a8c67d06',
|
||||
pubkey: '0xa6d310dbbfab9a22450f59993f87a4ce5db6223f3b5f1f30d2c4ec718922d400e0b3c7741de8e59960f72411a0ee10a7'}
|
||||
- {privkey: '0x2b3b88a041168a1c4cd04bdd8de7964fd35238f95442dc678514f9dadb81ec34',
|
||||
pubkey: '0x9893413c00283a3f9ed9fd9845dda1cea38228d22567f9541dccc357e54a2d6a6e204103c92564cbc05f4905ac7c493a'}
|
||||
133
common/eth2_interop_keypairs/src/lib.rs
Normal file
133
common/eth2_interop_keypairs/src/lib.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
//! Produces the "deterministic" validator private keys used for inter-operability testing for
|
||||
//! Ethereum 2.0 clients.
|
||||
//!
|
||||
//! Each private key is the sha2 hash of the validator index (little-endian, padded to 32 bytes),
|
||||
//! modulo the BLS-381 curve order.
|
||||
//!
|
||||
//! Keys generated here are **not secret** and are **not for production use**. It is trivial to
|
||||
//! know the secret key for any validator.
|
||||
//!
|
||||
//!## Reference
|
||||
//!
|
||||
//! Reference implementation:
|
||||
//!
|
||||
//! https://github.com/ethereum/eth2.0-pm/blob/6e41fcf383ebeb5125938850d8e9b4e9888389b4/interop/mocked_start/keygen.py
|
||||
//!
|
||||
//!
|
||||
//! This implementation passes the [reference implementation
|
||||
//! tests](https://github.com/ethereum/eth2.0-pm/blob/6e41fcf383ebeb5125938850d8e9b4e9888389b4/interop/mocked_start/keygen_test_vector.yaml).
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
use eth2_hashing::hash;
|
||||
use milagro_bls::{Keypair, PublicKey, SecretKey};
|
||||
use num_bigint::BigUint;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::convert::TryInto;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub const PRIVATE_KEY_BYTES: usize = 32;
|
||||
pub const PUBLIC_KEY_BYTES: usize = 48;
|
||||
pub const HASH_BYTES: usize = 32;
|
||||
|
||||
lazy_static! {
|
||||
static ref CURVE_ORDER: BigUint =
|
||||
"52435875175126190479447740508185965837690552500527637822603658699938581184513"
|
||||
.parse::<BigUint>()
|
||||
.expect("Curve order should be valid");
|
||||
}
|
||||
|
||||
/// Return a G1 point for the given `validator_index`, encoded as a compressed point in
|
||||
/// big-endian byte-ordering.
|
||||
pub fn be_private_key(validator_index: usize) -> [u8; PRIVATE_KEY_BYTES] {
|
||||
let preimage = {
|
||||
let mut bytes = [0; HASH_BYTES];
|
||||
let index = validator_index.to_le_bytes();
|
||||
bytes[0..index.len()].copy_from_slice(&index);
|
||||
bytes
|
||||
};
|
||||
|
||||
let privkey = BigUint::from_bytes_le(&hash(&preimage)) % &*CURVE_ORDER;
|
||||
|
||||
let mut bytes = [0; PRIVATE_KEY_BYTES];
|
||||
let privkey_bytes = privkey.to_bytes_be();
|
||||
bytes[PRIVATE_KEY_BYTES - privkey_bytes.len()..].copy_from_slice(&privkey_bytes);
|
||||
bytes
|
||||
}
|
||||
|
||||
/// Return a public and private keypair for a given `validator_index`.
|
||||
pub fn keypair(validator_index: usize) -> Keypair {
|
||||
let sk = SecretKey::from_bytes(&be_private_key(validator_index)).unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Should build valid private key for validator index {}",
|
||||
validator_index
|
||||
)
|
||||
});
|
||||
|
||||
Keypair {
|
||||
pk: PublicKey::from_secret_key(&sk),
|
||||
sk,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct YamlKeypair {
|
||||
/// Big-endian.
|
||||
privkey: String,
|
||||
/// Big-endian.
|
||||
pubkey: String,
|
||||
}
|
||||
|
||||
impl TryInto<Keypair> for YamlKeypair {
|
||||
type Error = String;
|
||||
|
||||
fn try_into(self) -> Result<Keypair, Self::Error> {
|
||||
let privkey = string_to_bytes(&self.privkey)?;
|
||||
let pubkey = string_to_bytes(&self.pubkey)?;
|
||||
|
||||
if (privkey.len() > PRIVATE_KEY_BYTES) || (pubkey.len() > PUBLIC_KEY_BYTES) {
|
||||
return Err("Public or private key is too long".into());
|
||||
}
|
||||
|
||||
let sk = {
|
||||
let mut bytes = vec![0; PRIVATE_KEY_BYTES - privkey.len()];
|
||||
bytes.extend_from_slice(&privkey);
|
||||
SecretKey::from_bytes(&bytes)
|
||||
.map_err(|e| format!("Failed to decode bytes into secret key: {:?}", e))?
|
||||
};
|
||||
|
||||
let pk = {
|
||||
let mut bytes = vec![0; PUBLIC_KEY_BYTES - pubkey.len()];
|
||||
bytes.extend_from_slice(&pubkey);
|
||||
PublicKey::from_bytes(&bytes)
|
||||
.map_err(|e| format!("Failed to decode bytes into public key: {:?}", e))?
|
||||
};
|
||||
|
||||
Ok(Keypair { pk, sk })
|
||||
}
|
||||
}
|
||||
|
||||
fn string_to_bytes(string: &str) -> Result<Vec<u8>, String> {
|
||||
let string = if string.starts_with("0x") {
|
||||
&string[2..]
|
||||
} else {
|
||||
string
|
||||
};
|
||||
|
||||
hex::decode(string).map_err(|e| format!("Unable to decode public or private key: {}", e))
|
||||
}
|
||||
|
||||
/// Loads keypairs from a YAML encoded file.
|
||||
///
|
||||
/// Uses this as reference:
|
||||
/// https://github.com/ethereum/eth2.0-pm/blob/9a9dbcd95e2b8e10287797bd768014ab3d842e99/interop/mocked_start/keygen_10_validators.yaml
|
||||
pub fn keypairs_from_yaml_file(path: PathBuf) -> Result<Vec<Keypair>, String> {
|
||||
let file = File::open(path).map_err(|e| format!("Unable to open YAML key file: {}", e))?;
|
||||
|
||||
serde_yaml::from_reader::<_, Vec<YamlKeypair>>(file)
|
||||
.map_err(|e| format!("Could not parse YAML: {:?}", e))?
|
||||
.into_iter()
|
||||
.map(TryInto::try_into)
|
||||
.collect::<Result<Vec<_>, String>>()
|
||||
}
|
||||
23
common/eth2_interop_keypairs/tests/from_file.rs
Normal file
23
common/eth2_interop_keypairs/tests/from_file.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
#![cfg(test)]
|
||||
use eth2_interop_keypairs::{keypair as reference_keypair, keypairs_from_yaml_file};
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn yaml_path() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("specs")
|
||||
.join("keygen_10_validators.yaml")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_from_yaml() {
|
||||
let keypairs = keypairs_from_yaml_file(yaml_path()).expect("should read keypairs from file");
|
||||
|
||||
keypairs.into_iter().enumerate().for_each(|(i, keypair)| {
|
||||
assert_eq!(
|
||||
keypair,
|
||||
reference_keypair(i),
|
||||
"Decoded key {} does not match generated key",
|
||||
i
|
||||
)
|
||||
});
|
||||
}
|
||||
58
common/eth2_interop_keypairs/tests/generation.rs
Normal file
58
common/eth2_interop_keypairs/tests/generation.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
#![cfg(test)]
|
||||
use eth2_interop_keypairs::{be_private_key, keypair};
|
||||
use num_bigint::BigUint;
|
||||
|
||||
#[test]
|
||||
fn reference_private_keys() {
|
||||
// Sourced from:
|
||||
//
|
||||
// https://github.com/ethereum/eth2.0-pm/blob/6e41fcf383ebeb5125938850d8e9b4e9888389b4/interop/mocked_start/keygen_test_vector.yaml
|
||||
let reference = [
|
||||
"16808672146709759238327133555736750089977066230599028589193936481731504400486",
|
||||
"37006103240406073079686739739280712467525465637222501547219594975923976982528",
|
||||
"22330876536127119444572216874798222843352868708084730796787004036811744442455",
|
||||
"17048462031355941381150076874414096388968985457797372268770826099852902060945",
|
||||
"28647806952216650698330424381872693846361470773871570637461872359310549743691",
|
||||
"2416304019107052589452838695606585506736351107897780798170812672519914514344",
|
||||
"7300215445567548136411883691093515822872548648751398235557229381530420545683",
|
||||
"26495790445032093722332687600112008700915252495659977774957922313678954054133",
|
||||
"2908643403277969554503670470854573663206729491025062456164283925661321952518",
|
||||
"19554639423851580804889717218680781396599791537051606512605582393920758869044",
|
||||
];
|
||||
reference.iter().enumerate().for_each(|(i, reference)| {
|
||||
let bytes = be_private_key(i);
|
||||
let num = BigUint::from_bytes_be(&bytes);
|
||||
assert_eq!(&num.to_str_radix(10), reference)
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_public_keys() {
|
||||
// Sourced from:
|
||||
//
|
||||
// https://github.com/ethereum/eth2.0-pm/blob/6e41fcf383ebeb5125938850d8e9b4e9888389b4/interop/mocked_start/keygen_test_vector.yaml
|
||||
let reference = [
|
||||
"qZp27XeW974i1bfoXe63xWd+iOUR4LM3YY+MTrYTSbS/LRU/ZJ97UzWf6LlKOORM",
|
||||
"uJvrxpl2lyajGMjplxvTFxKXxhrqSmV4p6T5S1R9y6W6wWqJEItrah/jaV0ah0oL",
|
||||
"o6MrD4tN24PxoKhT2B3XJd/ld9T0w9uOzlLOKwJuyoSBXBp+jpKk3j11VzO/fkqb",
|
||||
"iMFB33fNnY16cadcgmxBqcnwPG7hsYDz54UvaigAmd7TUbWNZuZTr45CgWpNj1Mu",
|
||||
"gSg7eiDhykYOvZu9dwBdVXNwyrsfmkT1MMTExmIw9nX434tMKBiFGqfXeoDKWkpe",
|
||||
"qwvdoPhfhC9DG+rM8SUL8f17pRtBAP1kNktkAf2oW7AGmz5xW1iBloTn/AsQpyo0",
|
||||
"mXfxyLcxqNVVgUa/uGyuomQ088WHi1ib8oCkLJFZ5wDp3w5AhilsILAR0ueMJ9Nz",
|
||||
"qNTHwneVpyWWExfvWVOnAy7W2Dc524sOinI1PRuLRDlCf376LInKoDzJ8o+Muris",
|
||||
"ptMQ27+rmiJFD1mZP4ekzl22Ij87Xx8w0sTscYki1ADgs8d0HejlmWD3JBGg7hCn",
|
||||
"mJNBPAAoOj+e2f2YRd2hzqOCKNIlZ/lUHczDV+VKLWpuIEEDySVky8BfSQWsfEk6",
|
||||
];
|
||||
reference.iter().enumerate().for_each(|(i, reference)| {
|
||||
let pair = keypair(i);
|
||||
let reference = base64::decode(reference).expect("Reference should be valid base64");
|
||||
|
||||
assert_eq!(
|
||||
reference.len(),
|
||||
48,
|
||||
"Reference should be 48 bytes (public key size)"
|
||||
);
|
||||
|
||||
assert_eq!(pair.pk.as_bytes(), reference);
|
||||
});
|
||||
}
|
||||
2
common/eth2_testnet_config/.gitignore
vendored
Normal file
2
common/eth2_testnet_config/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
testnet*
|
||||
schlesi-*
|
||||
20
common/eth2_testnet_config/Cargo.toml
Normal file
20
common/eth2_testnet_config/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "eth2_testnet_config"
|
||||
version = "0.2.0"
|
||||
authors = ["Paul Hauner <paul@paulhauner.com>"]
|
||||
edition = "2018"
|
||||
|
||||
build = "build.rs"
|
||||
|
||||
[build-dependencies]
|
||||
reqwest = { version = "0.10.4", features = ["blocking"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempdir = "0.3.7"
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0.110"
|
||||
serde_yaml = "0.8.11"
|
||||
types = { path = "../../consensus/types"}
|
||||
eth2-libp2p = { path = "../../beacon_node/eth2-libp2p"}
|
||||
eth2_ssz = "0.1.2"
|
||||
75
common/eth2_testnet_config/build.rs
Normal file
75
common/eth2_testnet_config/build.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
//! Downloads a testnet configuration from Github.
|
||||
|
||||
use reqwest;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const TESTNET_ID: &str = "schlesi-v0-11";
|
||||
|
||||
fn main() {
|
||||
if !base_dir().exists() {
|
||||
std::fs::create_dir_all(base_dir()).expect(&format!("Unable to create {:?}", base_dir()));
|
||||
|
||||
match get_all_files() {
|
||||
Ok(()) => (),
|
||||
Err(e) => {
|
||||
std::fs::remove_dir_all(base_dir()).expect(&format!(
|
||||
"{}. Failed to remove {:?}, please remove the directory manually because it may contains incomplete testnet data.",
|
||||
e,
|
||||
base_dir(),
|
||||
));
|
||||
panic!(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_all_files() -> Result<(), String> {
|
||||
get_file("boot_enr.yaml")?;
|
||||
get_file("config.yaml")?;
|
||||
get_file("deploy_block.txt")?;
|
||||
get_file("deposit_contract.txt")?;
|
||||
get_file("genesis.ssz")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_file(filename: &str) -> Result<(), String> {
|
||||
let url = format!(
|
||||
"https://raw.githubusercontent.com/goerli/schlesi/839866fe29a1b4df3a87bfe2ff1257c8a58671c9/light/{}",
|
||||
filename
|
||||
);
|
||||
|
||||
let path = base_dir().join(filename);
|
||||
let mut file =
|
||||
File::create(path).map_err(|e| format!("Failed to create {}: {:?}", filename, e))?;
|
||||
|
||||
let request = reqwest::blocking::Client::builder()
|
||||
.build()
|
||||
.map_err(|_| "Could not build request client".to_string())?
|
||||
.get(&url)
|
||||
.timeout(std::time::Duration::from_secs(120));
|
||||
|
||||
let contents = request
|
||||
.send()
|
||||
.map_err(|e| format!("Failed to download {}: {}", filename, e))?
|
||||
.error_for_status()
|
||||
.map_err(|e| format!("Error downloading {}: {}", filename, e))?
|
||||
.bytes()
|
||||
.map_err(|e| format!("Failed to read {} response bytes: {}", filename, e))?;
|
||||
|
||||
file.write(&contents)
|
||||
.map_err(|e| format!("Failed to write to {}: {:?}", filename, e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn base_dir() -> PathBuf {
|
||||
env::var("CARGO_MANIFEST_DIR")
|
||||
.expect("should know manifest dir")
|
||||
.parse::<PathBuf>()
|
||||
.expect("should parse manifest dir as path")
|
||||
.join(TESTNET_ID)
|
||||
}
|
||||
267
common/eth2_testnet_config/src/lib.rs
Normal file
267
common/eth2_testnet_config/src/lib.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
//! This crate should eventually represent the structure at this repo:
|
||||
//!
|
||||
//! https://github.com/eth2-clients/eth2-testnets/tree/master/nimbus/testnet1
|
||||
//!
|
||||
//! It is not accurate at the moment, we include extra files and we also don't support a few
|
||||
//! others. We are unable to conform to the repo until we have the following PR merged:
|
||||
//!
|
||||
//! https://github.com/sigp/lighthouse/pull/605
|
||||
|
||||
use eth2_libp2p::Enr;
|
||||
use ssz::{Decode, Encode};
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::PathBuf;
|
||||
use types::{Address, BeaconState, EthSpec, YamlConfig};
|
||||
|
||||
pub const ADDRESS_FILE: &str = "deposit_contract.txt";
|
||||
pub const DEPLOY_BLOCK_FILE: &str = "deploy_block.txt";
|
||||
pub const BOOT_ENR_FILE: &str = "boot_enr.yaml";
|
||||
pub const GENESIS_STATE_FILE: &str = "genesis.ssz";
|
||||
pub const YAML_CONFIG_FILE: &str = "config.yaml";
|
||||
|
||||
pub const HARDCODED_TESTNET: &str = "schlesi-v0-11";
|
||||
|
||||
pub const HARDCODED_YAML_CONFIG: &[u8] = include_bytes!("../schlesi-v0-11/config.yaml");
|
||||
pub const HARDCODED_DEPLOY_BLOCK: &[u8] = include_bytes!("../schlesi-v0-11/deploy_block.txt");
|
||||
pub const HARDCODED_DEPOSIT_CONTRACT: &[u8] =
|
||||
include_bytes!("../schlesi-v0-11/deposit_contract.txt");
|
||||
pub const HARDCODED_GENESIS_STATE: &[u8] = include_bytes!("../schlesi-v0-11/genesis.ssz");
|
||||
pub const HARDCODED_BOOT_ENR: &[u8] = include_bytes!("../schlesi-v0-11/boot_enr.yaml");
|
||||
|
||||
/// Specifies an Eth2 testnet.
|
||||
///
|
||||
/// See the crate-level documentation for more details.
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct Eth2TestnetConfig<E: EthSpec> {
|
||||
pub deposit_contract_address: String,
|
||||
pub deposit_contract_deploy_block: u64,
|
||||
pub boot_enr: Option<Vec<Enr>>,
|
||||
pub genesis_state: Option<BeaconState<E>>,
|
||||
pub yaml_config: Option<YamlConfig>,
|
||||
}
|
||||
|
||||
impl<E: EthSpec> Eth2TestnetConfig<E> {
|
||||
// Creates the `Eth2TestnetConfig` that was included in the binary at compile time. This can be
|
||||
// considered the default Lighthouse testnet.
|
||||
//
|
||||
// Returns an error if those included bytes are invalid (this is unlikely).
|
||||
pub fn hard_coded() -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
deposit_contract_address: serde_yaml::from_reader(HARDCODED_DEPOSIT_CONTRACT)
|
||||
.map_err(|e| format!("Unable to parse contract address: {:?}", e))?,
|
||||
deposit_contract_deploy_block: serde_yaml::from_reader(HARDCODED_DEPLOY_BLOCK)
|
||||
.map_err(|e| format!("Unable to parse deploy block: {:?}", e))?,
|
||||
boot_enr: Some(
|
||||
serde_yaml::from_reader(HARDCODED_BOOT_ENR)
|
||||
.map_err(|e| format!("Unable to parse boot enr: {:?}", e))?,
|
||||
),
|
||||
genesis_state: Some(
|
||||
BeaconState::from_ssz_bytes(HARDCODED_GENESIS_STATE)
|
||||
.map_err(|e| format!("Unable to parse genesis state: {:?}", e))?,
|
||||
),
|
||||
yaml_config: Some(
|
||||
serde_yaml::from_reader(HARDCODED_YAML_CONFIG)
|
||||
.map_err(|e| format!("Unable to parse genesis state: {:?}", e))?,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
// Write the files to the directory.
|
||||
//
|
||||
// Overwrites files if specified to do so.
|
||||
pub fn write_to_file(&self, base_dir: PathBuf, overwrite: bool) -> Result<(), String> {
|
||||
if base_dir.exists() && !overwrite {
|
||||
return Err("Testnet directory already exists".to_string());
|
||||
}
|
||||
|
||||
self.force_write_to_file(base_dir)
|
||||
}
|
||||
|
||||
// Write the files to the directory, even if the directory already exists.
|
||||
pub fn force_write_to_file(&self, base_dir: PathBuf) -> Result<(), String> {
|
||||
create_dir_all(&base_dir)
|
||||
.map_err(|e| format!("Unable to create testnet directory: {:?}", e))?;
|
||||
|
||||
macro_rules! write_to_yaml_file {
|
||||
($file: ident, $variable: expr) => {
|
||||
File::create(base_dir.join($file))
|
||||
.map_err(|e| format!("Unable to create {}: {:?}", $file, e))
|
||||
.and_then(|mut file| {
|
||||
let yaml = serde_yaml::to_string(&$variable)
|
||||
.map_err(|e| format!("Unable to YAML encode {}: {:?}", $file, e))?;
|
||||
|
||||
// Remove the doc header from the YAML file.
|
||||
//
|
||||
// This allows us to play nice with other clients that are expecting
|
||||
// plain-text, not YAML.
|
||||
let no_doc_header = if yaml.starts_with("---\n") {
|
||||
&yaml[4..]
|
||||
} else {
|
||||
&yaml
|
||||
};
|
||||
|
||||
file.write_all(no_doc_header.as_bytes())
|
||||
.map_err(|e| format!("Unable to write {}: {:?}", $file, e))
|
||||
})?;
|
||||
};
|
||||
}
|
||||
|
||||
write_to_yaml_file!(ADDRESS_FILE, self.deposit_contract_address);
|
||||
write_to_yaml_file!(DEPLOY_BLOCK_FILE, self.deposit_contract_deploy_block);
|
||||
|
||||
if let Some(boot_enr) = &self.boot_enr {
|
||||
write_to_yaml_file!(BOOT_ENR_FILE, boot_enr);
|
||||
}
|
||||
|
||||
if let Some(yaml_config) = &self.yaml_config {
|
||||
write_to_yaml_file!(YAML_CONFIG_FILE, yaml_config);
|
||||
}
|
||||
|
||||
// The genesis state is a special case because it uses SSZ, not YAML.
|
||||
if let Some(genesis_state) = &self.genesis_state {
|
||||
let file = base_dir.join(GENESIS_STATE_FILE);
|
||||
|
||||
File::create(&file)
|
||||
.map_err(|e| format!("Unable to create {:?}: {:?}", file, e))
|
||||
.and_then(|mut file| {
|
||||
file.write_all(&genesis_state.as_ssz_bytes())
|
||||
.map_err(|e| format!("Unable to write {:?}: {:?}", file, e))
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load(base_dir: PathBuf) -> Result<Self, String> {
|
||||
macro_rules! load_from_file {
|
||||
($file: ident) => {
|
||||
File::open(base_dir.join($file))
|
||||
.map_err(|e| format!("Unable to open {}: {:?}", $file, e))
|
||||
.and_then(|file| {
|
||||
serde_yaml::from_reader(file)
|
||||
.map_err(|e| format!("Unable to parse {}: {:?}", $file, e))
|
||||
})?;
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! optional_load_from_file {
|
||||
($file: ident) => {
|
||||
if base_dir.join($file).exists() {
|
||||
Some(load_from_file!($file))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let deposit_contract_address = load_from_file!(ADDRESS_FILE);
|
||||
let deposit_contract_deploy_block = load_from_file!(DEPLOY_BLOCK_FILE);
|
||||
let boot_enr = optional_load_from_file!(BOOT_ENR_FILE);
|
||||
let yaml_config = optional_load_from_file!(YAML_CONFIG_FILE);
|
||||
|
||||
// The genesis state is a special case because it uses SSZ, not YAML.
|
||||
let genesis_file_path = base_dir.join(GENESIS_STATE_FILE);
|
||||
let genesis_state = if genesis_file_path.exists() {
|
||||
Some(
|
||||
File::open(&genesis_file_path)
|
||||
.map_err(|e| format!("Unable to open {:?}: {:?}", genesis_file_path, e))
|
||||
.and_then(|mut file| {
|
||||
let mut bytes = vec![];
|
||||
file.read_to_end(&mut bytes)
|
||||
.map_err(|e| format!("Unable to read {:?}: {:?}", file, e))?;
|
||||
|
||||
BeaconState::from_ssz_bytes(&bytes)
|
||||
.map_err(|e| format!("Unable to SSZ decode {:?}: {:?}", file, e))
|
||||
})?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
deposit_contract_address,
|
||||
deposit_contract_deploy_block,
|
||||
boot_enr,
|
||||
genesis_state,
|
||||
yaml_config,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn deposit_contract_address(&self) -> Result<Address, String> {
|
||||
if self.deposit_contract_address.starts_with("0x") {
|
||||
self.deposit_contract_address[2..]
|
||||
.parse()
|
||||
.map_err(|e| format!("Corrupted address, unable to parse: {:?}", e))
|
||||
} else {
|
||||
Err("Corrupted address, must start with 0x".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempdir::TempDir;
|
||||
use types::{Eth1Data, Hash256, MainnetEthSpec, YamlConfig};
|
||||
|
||||
type E = MainnetEthSpec;
|
||||
|
||||
/* TODO: disabled until testnet config is updated for v0.11
|
||||
#[test]
|
||||
fn hard_coded_works() {
|
||||
let dir: Eth2TestnetConfig<E> =
|
||||
Eth2TestnetConfig::hard_coded().expect("should decode hard_coded params");
|
||||
|
||||
assert!(dir.boot_enr.is_some());
|
||||
assert!(dir.genesis_state.is_some());
|
||||
assert!(dir.yaml_config.is_some());
|
||||
}
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
let spec = &E::default_spec();
|
||||
|
||||
let eth1_data = Eth1Data {
|
||||
deposit_root: Hash256::zero(),
|
||||
deposit_count: 0,
|
||||
block_hash: Hash256::zero(),
|
||||
};
|
||||
|
||||
// TODO: figure out how to generate ENR and add some here.
|
||||
let boot_enr = None;
|
||||
let genesis_state = Some(BeaconState::new(42, eth1_data, spec));
|
||||
let yaml_config = Some(YamlConfig::from_spec::<E>(spec));
|
||||
|
||||
do_test::<E>(boot_enr, genesis_state, yaml_config);
|
||||
do_test::<E>(None, None, None);
|
||||
}
|
||||
|
||||
fn do_test<E: EthSpec>(
|
||||
boot_enr: Option<Vec<Enr>>,
|
||||
genesis_state: Option<BeaconState<E>>,
|
||||
yaml_config: Option<YamlConfig>,
|
||||
) {
|
||||
let temp_dir = TempDir::new("eth2_testnet_test").expect("should create temp dir");
|
||||
let base_dir = temp_dir.path().join("my_testnet");
|
||||
let deposit_contract_address = "0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413".to_string();
|
||||
let deposit_contract_deploy_block = 42;
|
||||
|
||||
let testnet: Eth2TestnetConfig<E> = Eth2TestnetConfig {
|
||||
deposit_contract_address,
|
||||
deposit_contract_deploy_block,
|
||||
boot_enr,
|
||||
genesis_state,
|
||||
yaml_config,
|
||||
};
|
||||
|
||||
testnet
|
||||
.write_to_file(base_dir.clone(), false)
|
||||
.expect("should write to file");
|
||||
|
||||
let decoded = Eth2TestnetConfig::load(base_dir).expect("should load struct");
|
||||
|
||||
assert_eq!(testnet, decoded, "should decode as encoded");
|
||||
}
|
||||
}
|
||||
14
common/eth2_wallet_manager/Cargo.toml
Normal file
14
common/eth2_wallet_manager/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "eth2_wallet_manager"
|
||||
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]
|
||||
eth2_keystore = { path = "../../crypto/eth2_keystore" }
|
||||
eth2_wallet = { path = "../../crypto/eth2_wallet" }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.1.0"
|
||||
97
common/eth2_wallet_manager/src/filesystem.rs
Normal file
97
common/eth2_wallet_manager/src/filesystem.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
//! Provides some CRUD functions for wallets on the filesystem.
|
||||
|
||||
use eth2_wallet::Error as WalletError;
|
||||
use eth2_wallet::{Uuid, Wallet};
|
||||
use std::fs::{copy as copy_file, remove_file, OpenOptions};
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
WalletAlreadyExists(PathBuf),
|
||||
WalletDoesNotExist(PathBuf),
|
||||
WalletBackupAlreadyExists(PathBuf),
|
||||
UnableToCreateBackup(io::Error),
|
||||
UnableToRemoveBackup(io::Error),
|
||||
UnableToRemoveWallet(io::Error),
|
||||
UnableToCreateWallet(io::Error),
|
||||
UnableToReadWallet(io::Error),
|
||||
JsonWriteError(WalletError),
|
||||
JsonReadError(WalletError),
|
||||
}
|
||||
|
||||
/// Read a wallet with the given `uuid` from the `wallet_dir`.
|
||||
pub fn read<P: AsRef<Path>>(wallet_dir: P, uuid: &Uuid) -> Result<Wallet, Error> {
|
||||
let json_path = wallet_json_path(wallet_dir, uuid);
|
||||
|
||||
if !json_path.exists() {
|
||||
Err(Error::WalletDoesNotExist(json_path))
|
||||
} else {
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.create(false)
|
||||
.open(json_path)
|
||||
.map_err(Error::UnableToReadWallet)
|
||||
.and_then(|f| Wallet::from_json_reader(f).map_err(Error::JsonReadError))
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the JSON file in the `wallet_dir` with the given `wallet`.
|
||||
///
|
||||
/// Performs a three-step copy:
|
||||
///
|
||||
/// 1. Copy the current JSON file to a backup file.
|
||||
/// 2. Over-write the existing JSON file.
|
||||
/// 3. Delete the backup file.
|
||||
pub fn update<P: AsRef<Path>>(wallet_dir: P, wallet: &Wallet) -> Result<(), Error> {
|
||||
let wallet_dir = wallet_dir.as_ref();
|
||||
|
||||
let json_path = wallet_json_path(wallet_dir, wallet.uuid());
|
||||
let json_backup_path = wallet_json_backup_path(wallet_dir, wallet.uuid());
|
||||
|
||||
// Require that a wallet already exists.
|
||||
if !json_path.exists() {
|
||||
return Err(Error::WalletDoesNotExist(json_path));
|
||||
// Require that there is no existing backup.
|
||||
} else if json_backup_path.exists() {
|
||||
return Err(Error::WalletBackupAlreadyExists(json_backup_path));
|
||||
}
|
||||
|
||||
// Copy the existing wallet to the backup location.
|
||||
copy_file(&json_path, &json_backup_path).map_err(Error::UnableToCreateBackup)?;
|
||||
|
||||
// Remove the existing wallet
|
||||
remove_file(json_path).map_err(Error::UnableToRemoveWallet)?;
|
||||
|
||||
// Create the new wallet.
|
||||
create(wallet_dir, wallet)?;
|
||||
|
||||
// Remove the backup file.
|
||||
remove_file(json_backup_path).map_err(Error::UnableToRemoveBackup)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Writes the `wallet` into the `wallet_dir`, returning an error if it already exists.
|
||||
pub fn create<P: AsRef<Path>>(wallet_dir: P, wallet: &Wallet) -> Result<(), Error> {
|
||||
let json_path = wallet_json_path(wallet_dir, wallet.uuid());
|
||||
|
||||
if json_path.exists() {
|
||||
Err(Error::WalletAlreadyExists(json_path))
|
||||
} else {
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(json_path)
|
||||
.map_err(Error::UnableToCreateWallet)
|
||||
.and_then(|f| wallet.to_json_writer(f).map_err(Error::JsonWriteError))
|
||||
}
|
||||
}
|
||||
|
||||
fn wallet_json_backup_path<P: AsRef<Path>>(wallet_dir: P, uuid: &Uuid) -> PathBuf {
|
||||
wallet_dir.as_ref().join(format!("{}.backup", uuid))
|
||||
}
|
||||
|
||||
fn wallet_json_path<P: AsRef<Path>>(wallet_dir: P, uuid: &Uuid) -> PathBuf {
|
||||
wallet_dir.as_ref().join(format!("{}", uuid))
|
||||
}
|
||||
6
common/eth2_wallet_manager/src/lib.rs
Normal file
6
common/eth2_wallet_manager/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
mod filesystem;
|
||||
mod locked_wallet;
|
||||
mod wallet_manager;
|
||||
|
||||
pub use locked_wallet::LockedWallet;
|
||||
pub use wallet_manager::{Error, WalletManager, WalletType};
|
||||
111
common/eth2_wallet_manager/src/locked_wallet.rs
Normal file
111
common/eth2_wallet_manager/src/locked_wallet.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use crate::{
|
||||
filesystem::{read, update},
|
||||
Error,
|
||||
};
|
||||
use eth2_wallet::{Uuid, ValidatorKeystores, Wallet};
|
||||
use std::fs::{remove_file, OpenOptions};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub const LOCK_FILE: &str = ".lock";
|
||||
|
||||
/// Represents a `Wallet` in a `wallet_dir`.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```ignore
|
||||
/// <wallet_dir>
|
||||
/// └── .lock
|
||||
/// └── <wallet-json>
|
||||
/// ```
|
||||
///
|
||||
/// Provides the following functionality:
|
||||
///
|
||||
/// - Control over the `.lock` file to prevent concurrent access.
|
||||
/// - A `next_validator` function which wraps `Wallet::next_validator`, ensuring that the wallet is
|
||||
/// persisted to disk (as JSON) between each consecutive call.
|
||||
pub struct LockedWallet {
|
||||
wallet_dir: PathBuf,
|
||||
wallet: Wallet,
|
||||
}
|
||||
|
||||
impl LockedWallet {
|
||||
/// Opens a wallet with the `uuid` from a `base_dir`.
|
||||
///
|
||||
/// ```ignore
|
||||
/// <base-dir>
|
||||
/// ├── <uuid (directory)>
|
||||
/// └── <uuid (json file)>
|
||||
/// ```
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// - If the wallet does not exist.
|
||||
/// - There is file-system or parsing error.
|
||||
/// - The lock-file already exists.
|
||||
pub(crate) fn open<P: AsRef<Path>>(base_dir: P, uuid: &Uuid) -> Result<Self, Error> {
|
||||
let wallet_dir = base_dir.as_ref().join(format!("{}", uuid));
|
||||
|
||||
if !wallet_dir.exists() {
|
||||
return Err(Error::MissingWalletDir(wallet_dir));
|
||||
}
|
||||
|
||||
let lockfile = wallet_dir.join(LOCK_FILE);
|
||||
if lockfile.exists() {
|
||||
return Err(Error::WalletIsLocked(wallet_dir));
|
||||
} else {
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(lockfile)
|
||||
.map_err(Error::UnableToCreateLockfile)?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
wallet: read(&wallet_dir, uuid)?,
|
||||
wallet_dir,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying wallet.
|
||||
///
|
||||
/// Note: this does not read from the file-system on each call. It assumes that the wallet does
|
||||
/// not change due to the use of a lock-file.
|
||||
pub fn wallet(&self) -> &Wallet {
|
||||
&self.wallet
|
||||
}
|
||||
|
||||
/// Calls `Wallet::next_validator` on the underlying `wallet`.
|
||||
///
|
||||
/// Ensures that the wallet JSON file is updated after each call.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// - If there is an error generating the validator keys.
|
||||
/// - If there is a file-system error.
|
||||
pub fn next_validator(
|
||||
&mut self,
|
||||
wallet_password: &[u8],
|
||||
voting_keystore_password: &[u8],
|
||||
withdrawal_keystore_password: &[u8],
|
||||
) -> Result<ValidatorKeystores, Error> {
|
||||
let keystores = self.wallet.next_validator(
|
||||
wallet_password,
|
||||
voting_keystore_password,
|
||||
withdrawal_keystore_password,
|
||||
)?;
|
||||
|
||||
update(&self.wallet_dir, &self.wallet)?;
|
||||
|
||||
Ok(keystores)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LockedWallet {
|
||||
/// Clean-up the lockfile.
|
||||
fn drop(&mut self) {
|
||||
let lockfile = self.wallet_dir.clone().join(LOCK_FILE);
|
||||
if let Err(e) = remove_file(&lockfile) {
|
||||
eprintln!("Unable to remove {:?}: {:?}", lockfile, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
380
common/eth2_wallet_manager/src/wallet_manager.rs
Normal file
380
common/eth2_wallet_manager/src/wallet_manager.rs
Normal file
@@ -0,0 +1,380 @@
|
||||
use crate::{
|
||||
filesystem::{create, Error as FilesystemError},
|
||||
LockedWallet,
|
||||
};
|
||||
use eth2_wallet::{bip39::Mnemonic, Error as WalletError, Uuid, Wallet, WalletBuilder};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
use std::fs::{create_dir_all, read_dir, OpenOptions};
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
DirectoryDoesNotExist(PathBuf),
|
||||
WalletError(WalletError),
|
||||
FilesystemError(FilesystemError),
|
||||
UnableToReadDir(io::Error),
|
||||
UnableToReadWallet(io::Error),
|
||||
UnableToReadFilename(OsString),
|
||||
NameAlreadyTaken(String),
|
||||
WalletNameUnknown(String),
|
||||
WalletDirExists(PathBuf),
|
||||
IoError(io::Error),
|
||||
WalletIsLocked(PathBuf),
|
||||
MissingWalletDir(PathBuf),
|
||||
UnableToCreateLockfile(io::Error),
|
||||
UuidMismatch((Uuid, Uuid)),
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(e: io::Error) -> Error {
|
||||
Error::IoError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WalletError> for Error {
|
||||
fn from(e: WalletError) -> Error {
|
||||
Error::WalletError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FilesystemError> for Error {
|
||||
fn from(e: FilesystemError) -> Error {
|
||||
Error::FilesystemError(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the type of an EIP-2386 wallet.
|
||||
///
|
||||
/// Presently only `Hd` wallets are supported.
|
||||
pub enum WalletType {
|
||||
/// Hierarchical-deterministic.
|
||||
Hd,
|
||||
}
|
||||
|
||||
/// Manages a directory containing EIP-2386 wallets.
|
||||
///
|
||||
/// Each wallet is stored in a directory with the name of the wallet UUID. Inside each directory a
|
||||
/// EIP-2386 JSON wallet is also stored using the UUID as the filename.
|
||||
///
|
||||
/// In each wallet directory an optional `.lock` exists to prevent concurrent reads and writes from
|
||||
/// the same wallet.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// ```ignore
|
||||
/// wallets
|
||||
/// ├── 35c07717-c6f3-45e8-976f-ef5d267e86c9
|
||||
/// │ └── 35c07717-c6f3-45e8-976f-ef5d267e86c9
|
||||
/// └── 747ad9dc-e1a1-4804-ada4-0dc124e46c49
|
||||
/// └── .lock
|
||||
/// └── 747ad9dc-e1a1-4804-ada4-0dc124e46c49
|
||||
/// ```
|
||||
pub struct WalletManager {
|
||||
dir: PathBuf,
|
||||
}
|
||||
|
||||
impl WalletManager {
|
||||
/// Open a directory containing multiple wallets.
|
||||
///
|
||||
/// Pass the `wallets` directory 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))
|
||||
}
|
||||
}
|
||||
|
||||
/// Searches all wallets in `self.dir` and returns the wallet with this name.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// - If there is no wallet with this name.
|
||||
/// - If there is a file-system or parsing error.
|
||||
pub fn wallet_by_name(&self, name: &str) -> Result<LockedWallet, Error> {
|
||||
LockedWallet::open(
|
||||
self.dir.clone(),
|
||||
self.wallets()?
|
||||
.get(name)
|
||||
.ok_or_else(|| Error::WalletNameUnknown(name.into()))?,
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new wallet with the given `name` in `self.dir` with the given `mnemonic` as a
|
||||
/// seed, encrypted with `password`.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// - If a wallet with this name already exists.
|
||||
/// - If there is a file-system or parsing error.
|
||||
pub fn create_wallet(
|
||||
&self,
|
||||
name: String,
|
||||
_wallet_type: WalletType,
|
||||
mnemonic: &Mnemonic,
|
||||
password: &[u8],
|
||||
) -> Result<LockedWallet, Error> {
|
||||
if self.wallets()?.contains_key(&name) {
|
||||
return Err(Error::NameAlreadyTaken(name));
|
||||
}
|
||||
|
||||
let wallet = WalletBuilder::from_mnemonic(mnemonic, password, name)?.build()?;
|
||||
let uuid = wallet.uuid().clone();
|
||||
|
||||
let wallet_dir = self.dir.join(format!("{}", uuid));
|
||||
|
||||
if wallet_dir.exists() {
|
||||
return Err(Error::WalletDirExists(wallet_dir));
|
||||
}
|
||||
|
||||
create_dir_all(&wallet_dir)?;
|
||||
|
||||
create(&wallet_dir, &wallet)?;
|
||||
|
||||
drop(wallet);
|
||||
|
||||
LockedWallet::open(&self.dir, &uuid)
|
||||
}
|
||||
|
||||
/// Iterates all wallets in `self.dir` and returns a mapping of their name to their UUID.
|
||||
///
|
||||
/// Ignores any items in `self.dir` that:
|
||||
///
|
||||
/// - Are files.
|
||||
/// - Are directories, but their file-name does not parse as a UUID.
|
||||
///
|
||||
/// This function is fairly strict, it will fail if any directory is found that does not obey
|
||||
/// the expected structure (e.g., there is a UUID directory that does not contain a valid JSON
|
||||
/// keystore with the same UUID).
|
||||
pub fn wallets(&self) -> Result<HashMap<String, Uuid>, Error> {
|
||||
let mut wallets = HashMap::new();
|
||||
|
||||
for f in read_dir(&self.dir).map_err(Error::UnableToReadDir)? {
|
||||
let f = f?;
|
||||
|
||||
// Ignore any non-directory objects in the root wallet dir.
|
||||
if f.file_type()?.is_dir() {
|
||||
let file_name = f
|
||||
.file_name()
|
||||
.into_string()
|
||||
.map_err(Error::UnableToReadFilename)?;
|
||||
|
||||
// Ignore any paths that don't parse as a UUID.
|
||||
if let Ok(uuid) = Uuid::parse_str(&file_name) {
|
||||
let wallet_path = f.path().join(format!("{}", uuid));
|
||||
let wallet = OpenOptions::new()
|
||||
.read(true)
|
||||
.create(false)
|
||||
.open(wallet_path)
|
||||
.map_err(Error::UnableToReadWallet)
|
||||
.and_then(|f| Wallet::from_json_reader(f).map_err(Error::WalletError))?;
|
||||
|
||||
if *wallet.uuid() != uuid {
|
||||
return Err(Error::UuidMismatch((uuid, *wallet.uuid())));
|
||||
}
|
||||
|
||||
wallets.insert(wallet.name().into(), *wallet.uuid());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(wallets)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
// These tests are very slow in debug, only test in release.
|
||||
#[cfg(not(debug_assertions))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{filesystem::read, locked_wallet::LOCK_FILE};
|
||||
use eth2_wallet::bip39::{Language, Mnemonic};
|
||||
use tempfile::tempdir;
|
||||
|
||||
const MNEMONIC: &str =
|
||||
"enemy fog enlist laundry nurse hungry discover turkey holiday resemble glad discover";
|
||||
const WALLET_PASSWORD: &[u8] = &[43; 43];
|
||||
|
||||
fn get_mnemonic() -> Mnemonic {
|
||||
Mnemonic::from_phrase(MNEMONIC, Language::English).unwrap()
|
||||
}
|
||||
|
||||
fn create_wallet(mgr: &WalletManager, id: usize) -> LockedWallet {
|
||||
let wallet = mgr
|
||||
.create_wallet(
|
||||
format!("{}", id),
|
||||
WalletType::Hd,
|
||||
&get_mnemonic(),
|
||||
WALLET_PASSWORD,
|
||||
)
|
||||
.expect("should create wallet");
|
||||
|
||||
assert!(
|
||||
wallet_dir_path(&mgr.dir, wallet.wallet().uuid()).exists(),
|
||||
"should have created wallet dir"
|
||||
);
|
||||
assert!(
|
||||
json_path(&mgr.dir, wallet.wallet().uuid()).exists(),
|
||||
"should have created json file"
|
||||
);
|
||||
assert!(
|
||||
lockfile_path(&mgr.dir, wallet.wallet().uuid()).exists(),
|
||||
"should have created lockfile"
|
||||
);
|
||||
|
||||
wallet
|
||||
}
|
||||
|
||||
fn load_wallet_raw<P: AsRef<Path>>(base_dir: P, uuid: &Uuid) -> Wallet {
|
||||
read(wallet_dir_path(base_dir, uuid), uuid).expect("should load raw json")
|
||||
}
|
||||
|
||||
fn wallet_dir_path<P: AsRef<Path>>(base_dir: P, uuid: &Uuid) -> PathBuf {
|
||||
let s = format!("{}", uuid);
|
||||
base_dir.as_ref().join(&s)
|
||||
}
|
||||
|
||||
fn lockfile_path<P: AsRef<Path>>(base_dir: P, uuid: &Uuid) -> PathBuf {
|
||||
let s = format!("{}", uuid);
|
||||
base_dir.as_ref().join(&s).join(LOCK_FILE)
|
||||
}
|
||||
|
||||
fn json_path<P: AsRef<Path>>(base_dir: P, uuid: &Uuid) -> PathBuf {
|
||||
let s = format!("{}", uuid);
|
||||
base_dir.as_ref().join(&s).join(&s)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_names() {
|
||||
let dir = tempdir().unwrap();
|
||||
let base_dir = dir.path();
|
||||
let mgr = WalletManager::open(base_dir).unwrap();
|
||||
let name = "cats".to_string();
|
||||
|
||||
mgr.create_wallet(
|
||||
name.clone(),
|
||||
WalletType::Hd,
|
||||
&get_mnemonic(),
|
||||
WALLET_PASSWORD,
|
||||
)
|
||||
.expect("should create first wallet");
|
||||
|
||||
match mgr.create_wallet(
|
||||
name.clone(),
|
||||
WalletType::Hd,
|
||||
&get_mnemonic(),
|
||||
WALLET_PASSWORD,
|
||||
) {
|
||||
Err(Error::NameAlreadyTaken(_)) => {}
|
||||
_ => panic!("expected name error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keystore_generation() {
|
||||
let dir = tempdir().unwrap();
|
||||
let base_dir = dir.path();
|
||||
let mgr = WalletManager::open(base_dir).unwrap();
|
||||
let name = "cats".to_string();
|
||||
|
||||
let mut w = mgr
|
||||
.create_wallet(
|
||||
name.clone(),
|
||||
WalletType::Hd,
|
||||
&get_mnemonic(),
|
||||
WALLET_PASSWORD,
|
||||
)
|
||||
.expect("should create first wallet");
|
||||
|
||||
let uuid = w.wallet().uuid().clone();
|
||||
|
||||
assert_eq!(
|
||||
load_wallet_raw(&base_dir, &uuid).nextaccount(),
|
||||
0,
|
||||
"should start wallet with nextaccount 0"
|
||||
);
|
||||
|
||||
for i in 1..3 {
|
||||
w.next_validator(WALLET_PASSWORD, &[1], &[0])
|
||||
.expect("should create validator");
|
||||
assert_eq!(
|
||||
load_wallet_raw(&base_dir, &uuid).nextaccount(),
|
||||
i,
|
||||
"should update wallet with nextaccount {}",
|
||||
i
|
||||
);
|
||||
}
|
||||
|
||||
drop(w);
|
||||
|
||||
// Check that we can open the wallet by name.
|
||||
let by_name = mgr.wallet_by_name(&name).unwrap();
|
||||
assert_eq!(by_name.wallet().name(), name);
|
||||
|
||||
drop(by_name);
|
||||
|
||||
let wallets = mgr.wallets().unwrap().into_iter().collect::<Vec<_>>();
|
||||
assert_eq!(wallets, vec![(name, uuid)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn locked_wallet_lockfile() {
|
||||
let dir = tempdir().unwrap();
|
||||
let base_dir = dir.path();
|
||||
let mgr = WalletManager::open(base_dir).unwrap();
|
||||
|
||||
let uuid_a = create_wallet(&mgr, 0).wallet().uuid().clone();
|
||||
let uuid_b = create_wallet(&mgr, 1).wallet().uuid().clone();
|
||||
|
||||
let locked_a = LockedWallet::open(&base_dir, &uuid_a).expect("should open wallet a");
|
||||
|
||||
assert!(
|
||||
lockfile_path(&base_dir, &uuid_a).exists(),
|
||||
"lockfile should exist"
|
||||
);
|
||||
|
||||
drop(locked_a);
|
||||
|
||||
assert!(
|
||||
!lockfile_path(&base_dir, &uuid_a).exists(),
|
||||
"lockfile have been cleaned up"
|
||||
);
|
||||
|
||||
let locked_a = LockedWallet::open(&base_dir, &uuid_a).expect("should open wallet a");
|
||||
let locked_b = LockedWallet::open(&base_dir, &uuid_b).expect("should open wallet b");
|
||||
|
||||
assert!(
|
||||
lockfile_path(&base_dir, &uuid_a).exists(),
|
||||
"lockfile a should exist"
|
||||
);
|
||||
|
||||
assert!(
|
||||
lockfile_path(&base_dir, &uuid_b).exists(),
|
||||
"lockfile b should exist"
|
||||
);
|
||||
|
||||
match LockedWallet::open(&base_dir, &uuid_a) {
|
||||
Err(Error::WalletIsLocked(_)) => {}
|
||||
_ => panic!("did not get locked error"),
|
||||
};
|
||||
|
||||
drop(locked_a);
|
||||
|
||||
LockedWallet::open(&base_dir, &uuid_a)
|
||||
.expect("should open wallet a after previous instance is dropped");
|
||||
|
||||
match LockedWallet::open(&base_dir, &uuid_b) {
|
||||
Err(Error::WalletIsLocked(_)) => {}
|
||||
_ => panic!("did not get locked error"),
|
||||
};
|
||||
|
||||
drop(locked_b);
|
||||
|
||||
LockedWallet::open(&base_dir, &uuid_b)
|
||||
.expect("should open wallet a after previous instance is dropped");
|
||||
}
|
||||
}
|
||||
12
common/hashset_delay/Cargo.toml
Normal file
12
common/hashset_delay/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "hashset_delay"
|
||||
version = "0.2.0"
|
||||
authors = ["Age Manning <Age@AgeManning.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3.5"
|
||||
tokio = { version = "0.2.20", features = ["time"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "0.2.20", features = ["time", "rt-threaded", "macros"] }
|
||||
192
common/hashset_delay/src/hashset_delay.rs
Normal file
192
common/hashset_delay/src/hashset_delay.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
//NOTE: This is just a specific case of a HashMapDelay.
|
||||
// The code has been copied to make unique `insert` and `insert_at` functions.
|
||||
|
||||
/// The default delay for entries, in seconds. This is only used when `insert()` is used to add
|
||||
/// entries.
|
||||
const DEFAULT_DELAY: u64 = 30;
|
||||
|
||||
use futures::prelude::*;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::time::delay_queue::{self, DelayQueue};
|
||||
|
||||
pub struct HashSetDelay<K>
|
||||
where
|
||||
K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin,
|
||||
{
|
||||
/// The given entries.
|
||||
entries: HashMap<K, MapEntry>,
|
||||
/// A queue holding the timeouts of each entry.
|
||||
expirations: DelayQueue<K>,
|
||||
/// The default expiration timeout of an entry.
|
||||
default_entry_timeout: Duration,
|
||||
}
|
||||
|
||||
/// A wrapping around entries that adds the link to the entry's expiration, via a `delay_queue` key.
|
||||
struct MapEntry {
|
||||
/// The expiration key for the entry.
|
||||
key: delay_queue::Key,
|
||||
/// The actual entry.
|
||||
value: Instant,
|
||||
}
|
||||
|
||||
impl<K> Default for HashSetDelay<K>
|
||||
where
|
||||
K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin,
|
||||
{
|
||||
fn default() -> Self {
|
||||
HashSetDelay::new(Duration::from_secs(DEFAULT_DELAY))
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> HashSetDelay<K>
|
||||
where
|
||||
K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin,
|
||||
{
|
||||
/// Creates a new instance of `HashSetDelay`.
|
||||
pub fn new(default_entry_timeout: Duration) -> Self {
|
||||
HashSetDelay {
|
||||
entries: HashMap::new(),
|
||||
expirations: DelayQueue::new(),
|
||||
default_entry_timeout,
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert an entry into the mapping. Entries will expire after the `default_entry_timeout`.
|
||||
pub fn insert(&mut self, key: K) {
|
||||
self.insert_at(key, self.default_entry_timeout);
|
||||
}
|
||||
|
||||
/// Inserts an entry that will expire at a given instant. If the entry already exists, the
|
||||
/// timeout is updated.
|
||||
pub fn insert_at(&mut self, key: K, entry_duration: Duration) {
|
||||
if self.contains(&key) {
|
||||
// update the timeout
|
||||
self.update_timeout(&key, entry_duration);
|
||||
} else {
|
||||
let delay_key = self.expirations.insert(key.clone(), entry_duration.clone());
|
||||
let entry = MapEntry {
|
||||
key: delay_key,
|
||||
value: Instant::now() + entry_duration,
|
||||
};
|
||||
self.entries.insert(key, entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a reference to an entry if it exists.
|
||||
///
|
||||
/// Returns None if the entry does not exist.
|
||||
pub fn get(&self, key: &K) -> Option<&Instant> {
|
||||
self.entries.get(key).map(|entry| &entry.value)
|
||||
}
|
||||
|
||||
/// Returns true if the key exists, false otherwise.
|
||||
pub fn contains(&self, key: &K) -> bool {
|
||||
self.entries.contains_key(key)
|
||||
}
|
||||
|
||||
/// Returns the length of the mapping.
|
||||
pub fn len(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
/// Updates the timeout for a given key. Returns true if the key existed, false otherwise.
|
||||
///
|
||||
/// Panics if the duration is too far in the future.
|
||||
pub fn update_timeout(&mut self, key: &K, timeout: Duration) -> bool {
|
||||
if let Some(entry) = self.entries.get(key) {
|
||||
self.expirations.reset(&entry.key, timeout);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a key from the map returning the value associated with the key that was in the map.
|
||||
///
|
||||
/// Return false if the key was not in the map.
|
||||
pub fn remove(&mut self, key: &K) -> bool {
|
||||
if let Some(entry) = self.entries.remove(key) {
|
||||
self.expirations.remove(&entry.key);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Retains only the elements specified by the predicate.
|
||||
///
|
||||
/// In other words, remove all pairs `(k, v)` such that `f(&k,&mut v)` returns false.
|
||||
pub fn retain<F: FnMut(&K) -> bool>(&mut self, mut f: F) {
|
||||
let expiration = &mut self.expirations;
|
||||
self.entries.retain(|key, entry| {
|
||||
let result = f(key);
|
||||
if !result {
|
||||
expiration.remove(&entry.key);
|
||||
}
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes all entries from the map.
|
||||
pub fn clear(&mut self) {
|
||||
self.entries.clear();
|
||||
self.expirations.clear();
|
||||
}
|
||||
|
||||
/// Returns a vector of referencing all keys in the map.
|
||||
pub fn keys(&self) -> impl Iterator<Item = &K> {
|
||||
self.entries.keys()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> Stream for HashSetDelay<K>
|
||||
where
|
||||
K: std::cmp::Eq + std::hash::Hash + std::clone::Clone + Unpin,
|
||||
{
|
||||
type Item = Result<K, String>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
|
||||
match self.expirations.poll_expired(cx) {
|
||||
Poll::Ready(Some(Ok(key))) => match self.entries.remove(key.get_ref()) {
|
||||
Some(_) => Poll::Ready(Some(Ok(key.into_inner()))),
|
||||
None => Poll::Ready(Some(Err("Value no longer exists in expirations".into()))),
|
||||
},
|
||||
Poll::Ready(Some(Err(e))) => {
|
||||
Poll::Ready(Some(Err(format!("delay queue error: {:?}", e))))
|
||||
}
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn should_not_panic() {
|
||||
let key = 2u8;
|
||||
|
||||
let mut map = HashSetDelay::default();
|
||||
|
||||
map.insert(key);
|
||||
map.update_timeout(&key, Duration::from_secs(100));
|
||||
|
||||
let fut = |cx: &mut Context| {
|
||||
let _ = map.poll_next_unpin(cx);
|
||||
let _ = map.poll_next_unpin(cx);
|
||||
Poll::Ready(())
|
||||
};
|
||||
|
||||
future::poll_fn(fut).await;
|
||||
|
||||
map.insert(key);
|
||||
map.update_timeout(&key, Duration::from_secs(100));
|
||||
}
|
||||
}
|
||||
12
common/hashset_delay/src/lib.rs
Normal file
12
common/hashset_delay/src/lib.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
//! This crate provides a single type (its counter-part HashMapDelay has been removed as it
|
||||
//! currently is not in use in lighthouse):
|
||||
//! - `HashSetDelay`
|
||||
//!
|
||||
//! # HashSetDelay
|
||||
//!
|
||||
//! This is similar to a `HashMapDelay` except the mapping maps to the expiry time. This
|
||||
//! allows users to add objects and check their expiry deadlines before the `Stream`
|
||||
//! consumes them.
|
||||
|
||||
mod hashset_delay;
|
||||
pub use crate::hashset_delay::HashSetDelay;
|
||||
11
common/lighthouse_metrics/Cargo.toml
Normal file
11
common/lighthouse_metrics/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "lighthouse_metrics"
|
||||
version = "0.2.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]
|
||||
lazy_static = "1.4.0"
|
||||
prometheus = "0.8.0"
|
||||
159
common/lighthouse_metrics/src/lib.rs
Normal file
159
common/lighthouse_metrics/src/lib.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
#![allow(clippy::needless_doctest_main)]
|
||||
//! A wrapper around the `prometheus` crate that provides a global, `lazy_static` metrics registry
|
||||
//! and functions to add and use the following components (more info at
|
||||
//! [Prometheus docs](https://prometheus.io/docs/concepts/metric_types/)):
|
||||
//!
|
||||
//! - `Histogram`: used with `start_timer(..)` and `stop_timer(..)` to record durations (e.g.,
|
||||
//! block processing time).
|
||||
//! - `IncCounter`: used to represent an ideally ever-growing, never-shrinking integer (e.g.,
|
||||
//! number of block processing requests).
|
||||
//! - `IntGauge`: used to represent an varying integer (e.g., number of attestations per block).
|
||||
//!
|
||||
//! ## Important
|
||||
//!
|
||||
//! Metrics will fail if two items have the same `name`. All metrics must have a unique `name`.
|
||||
//! Because we use a global registry there is no namespace per crate, it's one big global space.
|
||||
//!
|
||||
//! See the [Prometheus naming best practices](https://prometheus.io/docs/practices/naming/) when
|
||||
//! choosing metric names.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! #[macro_use]
|
||||
//! extern crate lazy_static;
|
||||
//! use lighthouse_metrics::*;
|
||||
//!
|
||||
//! // These metrics are "magically" linked to the global registry defined in `lighthouse_metrics`.
|
||||
//! lazy_static! {
|
||||
//! pub static ref RUN_COUNT: Result<IntCounter> = try_create_int_counter(
|
||||
//! "runs_total",
|
||||
//! "Total number of runs"
|
||||
//! );
|
||||
//! pub static ref CURRENT_VALUE: Result<IntGauge> = try_create_int_gauge(
|
||||
//! "current_value",
|
||||
//! "The current value"
|
||||
//! );
|
||||
//! pub static ref RUN_TIME: Result<Histogram> =
|
||||
//! try_create_histogram("run_seconds", "Time taken (measured to high precision)");
|
||||
//! }
|
||||
//!
|
||||
//!
|
||||
//! fn main() {
|
||||
//! for i in 0..100 {
|
||||
//! inc_counter(&RUN_COUNT);
|
||||
//! let timer = start_timer(&RUN_TIME);
|
||||
//!
|
||||
//! for j in 0..10 {
|
||||
//! set_gauge(&CURRENT_VALUE, j);
|
||||
//! println!("Howdy partner");
|
||||
//! }
|
||||
//!
|
||||
//! stop_timer(timer);
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
|
||||
use prometheus::{HistogramOpts, HistogramTimer, Opts};
|
||||
|
||||
pub use prometheus::{Encoder, Gauge, Histogram, IntCounter, IntGauge, Result, TextEncoder};
|
||||
|
||||
/// Collect all the metrics for reporting.
|
||||
pub fn gather() -> Vec<prometheus::proto::MetricFamily> {
|
||||
prometheus::gather()
|
||||
}
|
||||
|
||||
/// Attempts to crate an `IntCounter`, returning `Err` if the registry does not accept the counter
|
||||
/// (potentially due to naming conflict).
|
||||
pub fn try_create_int_counter(name: &str, help: &str) -> Result<IntCounter> {
|
||||
let opts = Opts::new(name, help);
|
||||
let counter = IntCounter::with_opts(opts)?;
|
||||
prometheus::register(Box::new(counter.clone()))?;
|
||||
Ok(counter)
|
||||
}
|
||||
|
||||
/// Attempts to crate an `IntGauge`, returning `Err` if the registry does not accept the counter
|
||||
/// (potentially due to naming conflict).
|
||||
pub fn try_create_int_gauge(name: &str, help: &str) -> Result<IntGauge> {
|
||||
let opts = Opts::new(name, help);
|
||||
let gauge = IntGauge::with_opts(opts)?;
|
||||
prometheus::register(Box::new(gauge.clone()))?;
|
||||
Ok(gauge)
|
||||
}
|
||||
|
||||
/// Attempts to crate a `Gauge`, returning `Err` if the registry does not accept the counter
|
||||
/// (potentially due to naming conflict).
|
||||
pub fn try_create_float_gauge(name: &str, help: &str) -> Result<Gauge> {
|
||||
let opts = Opts::new(name, help);
|
||||
let gauge = Gauge::with_opts(opts)?;
|
||||
prometheus::register(Box::new(gauge.clone()))?;
|
||||
Ok(gauge)
|
||||
}
|
||||
|
||||
/// Attempts to crate a `Histogram`, returning `Err` if the registry does not accept the counter
|
||||
/// (potentially due to naming conflict).
|
||||
pub fn try_create_histogram(name: &str, help: &str) -> Result<Histogram> {
|
||||
let opts = HistogramOpts::new(name, help);
|
||||
let histogram = Histogram::with_opts(opts)?;
|
||||
prometheus::register(Box::new(histogram.clone()))?;
|
||||
Ok(histogram)
|
||||
}
|
||||
|
||||
/// Starts a timer for the given `Histogram`, stopping when it gets dropped or given to `stop_timer(..)`.
|
||||
pub fn start_timer(histogram: &Result<Histogram>) -> Option<HistogramTimer> {
|
||||
if let Ok(histogram) = histogram {
|
||||
Some(histogram.start_timer())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops a timer created with `start_timer(..)`.
|
||||
pub fn stop_timer(timer: Option<HistogramTimer>) {
|
||||
if let Some(t) = timer {
|
||||
t.observe_duration()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inc_counter(counter: &Result<IntCounter>) {
|
||||
if let Ok(counter) = counter {
|
||||
counter.inc();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inc_counter_by(counter: &Result<IntCounter>, value: i64) {
|
||||
if let Ok(counter) = counter {
|
||||
counter.inc_by(value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_gauge(gauge: &Result<IntGauge>, value: i64) {
|
||||
if let Ok(gauge) = gauge {
|
||||
gauge.set(value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maybe_set_gauge(gauge: &Result<IntGauge>, value_opt: Option<i64>) {
|
||||
if let Some(value) = value_opt {
|
||||
set_gauge(gauge, value)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_float_gauge(gauge: &Result<Gauge>, value: f64) {
|
||||
if let Ok(gauge) = gauge {
|
||||
gauge.set(value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maybe_set_float_gauge(gauge: &Result<Gauge>, value_opt: Option<f64>) {
|
||||
if let Some(value) = value_opt {
|
||||
set_float_gauge(gauge, value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the value of a `Histogram` manually.
|
||||
pub fn observe(histogram: &Result<Histogram>, value: f64) {
|
||||
if let Ok(histogram) = histogram {
|
||||
histogram.observe(value);
|
||||
}
|
||||
}
|
||||
11
common/logging/Cargo.toml
Normal file
11
common/logging/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "logging"
|
||||
version = "0.2.0"
|
||||
authors = ["blacktemplar <blacktemplar@a1.net>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
slog = "2.5.2"
|
||||
slog-term = "2.5.0"
|
||||
lighthouse_metrics = { path = "../lighthouse_metrics" }
|
||||
lazy_static = "1.4.0"
|
||||
160
common/logging/src/lib.rs
Normal file
160
common/logging/src/lib.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
use lighthouse_metrics::{
|
||||
inc_counter, try_create_int_counter, IntCounter, Result as MetricsResult,
|
||||
};
|
||||
use std::io::{Result, Write};
|
||||
|
||||
pub const MAX_MESSAGE_WIDTH: usize = 40;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref INFOS_TOTAL: MetricsResult<IntCounter> =
|
||||
try_create_int_counter("info_total", "Count of infos logged");
|
||||
pub static ref WARNS_TOTAL: MetricsResult<IntCounter> =
|
||||
try_create_int_counter("warn_total", "Count of warns logged");
|
||||
pub static ref ERRORS_TOTAL: MetricsResult<IntCounter> =
|
||||
try_create_int_counter("error_total", "Count of errors logged");
|
||||
pub static ref CRITS_TOTAL: MetricsResult<IntCounter> =
|
||||
try_create_int_counter("crit_total", "Count of crits logged");
|
||||
}
|
||||
|
||||
pub struct AlignedTermDecorator {
|
||||
wrapped: slog_term::TermDecorator,
|
||||
message_width: usize,
|
||||
}
|
||||
|
||||
impl AlignedTermDecorator {
|
||||
pub fn new(decorator: slog_term::TermDecorator, message_width: usize) -> AlignedTermDecorator {
|
||||
AlignedTermDecorator {
|
||||
wrapped: decorator,
|
||||
message_width,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl slog_term::Decorator for AlignedTermDecorator {
|
||||
fn with_record<F>(
|
||||
&self,
|
||||
record: &slog::Record,
|
||||
_logger_values: &slog::OwnedKVList,
|
||||
f: F,
|
||||
) -> Result<()>
|
||||
where
|
||||
F: FnOnce(&mut dyn slog_term::RecordDecorator) -> std::io::Result<()>,
|
||||
{
|
||||
match record.level() {
|
||||
slog::Level::Info => inc_counter(&INFOS_TOTAL),
|
||||
slog::Level::Warning => inc_counter(&WARNS_TOTAL),
|
||||
slog::Level::Error => inc_counter(&ERRORS_TOTAL),
|
||||
slog::Level::Critical => inc_counter(&CRITS_TOTAL),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
self.wrapped.with_record(record, _logger_values, |deco| {
|
||||
f(&mut AlignedRecordDecorator::new(deco, self.message_width))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct AlignedRecordDecorator<'a> {
|
||||
wrapped: &'a mut dyn slog_term::RecordDecorator,
|
||||
message_count: usize,
|
||||
message_active: bool,
|
||||
ignore_comma: bool,
|
||||
message_width: usize,
|
||||
}
|
||||
|
||||
impl<'a> AlignedRecordDecorator<'a> {
|
||||
fn new(
|
||||
decorator: &'a mut dyn slog_term::RecordDecorator,
|
||||
message_width: usize,
|
||||
) -> AlignedRecordDecorator<'a> {
|
||||
AlignedRecordDecorator {
|
||||
wrapped: decorator,
|
||||
message_count: 0,
|
||||
ignore_comma: false,
|
||||
message_active: false,
|
||||
message_width,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Write for AlignedRecordDecorator<'a> {
|
||||
fn write(&mut self, buf: &[u8]) -> Result<usize> {
|
||||
if self.ignore_comma {
|
||||
//don't write comma
|
||||
self.ignore_comma = false;
|
||||
Ok(buf.len())
|
||||
} else if self.message_active {
|
||||
self.wrapped.write(buf).map(|n| {
|
||||
self.message_count += n;
|
||||
n
|
||||
})
|
||||
} else {
|
||||
self.wrapped.write(buf)
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<()> {
|
||||
self.wrapped.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> slog_term::RecordDecorator for AlignedRecordDecorator<'a> {
|
||||
fn reset(&mut self) -> Result<()> {
|
||||
self.message_active = false;
|
||||
self.message_count = 0;
|
||||
self.ignore_comma = false;
|
||||
self.wrapped.reset()
|
||||
}
|
||||
|
||||
fn start_whitespace(&mut self) -> Result<()> {
|
||||
self.wrapped.start_whitespace()
|
||||
}
|
||||
|
||||
fn start_msg(&mut self) -> Result<()> {
|
||||
self.message_active = true;
|
||||
self.ignore_comma = false;
|
||||
self.wrapped.start_msg()
|
||||
}
|
||||
|
||||
fn start_timestamp(&mut self) -> Result<()> {
|
||||
self.wrapped.start_timestamp()
|
||||
}
|
||||
|
||||
fn start_level(&mut self) -> Result<()> {
|
||||
self.wrapped.start_level()
|
||||
}
|
||||
|
||||
fn start_comma(&mut self) -> Result<()> {
|
||||
if self.message_active && self.message_count + 1 < self.message_width {
|
||||
self.ignore_comma = true;
|
||||
}
|
||||
self.wrapped.start_comma()
|
||||
}
|
||||
|
||||
fn start_key(&mut self) -> Result<()> {
|
||||
if self.message_active && self.message_count + 1 < self.message_width {
|
||||
write!(
|
||||
self,
|
||||
"{}",
|
||||
std::iter::repeat(' ')
|
||||
.take(self.message_width - self.message_count)
|
||||
.collect::<String>()
|
||||
)?;
|
||||
self.message_active = false;
|
||||
self.message_count = 0;
|
||||
self.ignore_comma = false;
|
||||
}
|
||||
self.wrapped.start_key()
|
||||
}
|
||||
|
||||
fn start_value(&mut self) -> Result<()> {
|
||||
self.wrapped.start_value()
|
||||
}
|
||||
|
||||
fn start_separator(&mut self) -> Result<()> {
|
||||
self.wrapped.start_separator()
|
||||
}
|
||||
}
|
||||
21
common/remote_beacon_node/Cargo.toml
Normal file
21
common/remote_beacon_node/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "remote_beacon_node"
|
||||
version = "0.2.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]
|
||||
reqwest = { version = "0.10.4", features = ["json"] }
|
||||
url = "2.1.1"
|
||||
serde = "1.0.110"
|
||||
futures = "0.3.5"
|
||||
types = { path = "../../consensus/types" }
|
||||
rest_types = { path = "../rest_types" }
|
||||
hex = "0.4.2"
|
||||
eth2_ssz = "0.1.2"
|
||||
serde_json = "1.0.52"
|
||||
eth2_config = { path = "../eth2_config" }
|
||||
proto_array_fork_choice = { path = "../../consensus/proto_array_fork_choice" }
|
||||
operation_pool = { path = "../../beacon_node/operation_pool" }
|
||||
723
common/remote_beacon_node/src/lib.rs
Normal file
723
common/remote_beacon_node/src/lib.rs
Normal file
@@ -0,0 +1,723 @@
|
||||
//! Provides a `RemoteBeaconNode` which interacts with a HTTP API on another Lighthouse (or
|
||||
//! compatible) instance.
|
||||
//!
|
||||
//! Presently, this is only used for testing but it _could_ become a user-facing library.
|
||||
|
||||
use eth2_config::Eth2Config;
|
||||
use reqwest::{Client, ClientBuilder, Response, StatusCode};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use ssz::Encode;
|
||||
use std::marker::PhantomData;
|
||||
use std::time::Duration;
|
||||
use types::{
|
||||
Attestation, AttestationData, AttesterSlashing, BeaconBlock, BeaconState, CommitteeIndex,
|
||||
Epoch, EthSpec, Fork, Hash256, ProposerSlashing, PublicKey, PublicKeyBytes, Signature,
|
||||
SignedAggregateAndProof, SignedBeaconBlock, Slot,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
pub use operation_pool::PersistedOperationPool;
|
||||
pub use proto_array_fork_choice::core::ProtoArray;
|
||||
pub use rest_types::{
|
||||
CanonicalHeadResponse, Committee, HeadBeaconBlock, IndividualVotesRequest,
|
||||
IndividualVotesResponse, SyncingResponse, ValidatorDutiesRequest, ValidatorDutyBytes,
|
||||
ValidatorRequest, ValidatorResponse, ValidatorSubscription,
|
||||
};
|
||||
|
||||
// Setting a long timeout for debug ensures that crypto-heavy operations can still succeed.
|
||||
#[cfg(debug_assertions)]
|
||||
pub const REQUEST_TIMEOUT_SECONDS: u64 = 15;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub const REQUEST_TIMEOUT_SECONDS: u64 = 5;
|
||||
|
||||
#[derive(Clone)]
|
||||
/// Connects to a remote Lighthouse (or compatible) node via HTTP.
|
||||
pub struct RemoteBeaconNode<E: EthSpec> {
|
||||
pub http: HttpClient<E>,
|
||||
}
|
||||
|
||||
impl<E: EthSpec> RemoteBeaconNode<E> {
|
||||
/// Uses the default HTTP timeout.
|
||||
pub fn new(http_endpoint: String) -> Result<Self, String> {
|
||||
Self::new_with_timeout(http_endpoint, Duration::from_secs(REQUEST_TIMEOUT_SECONDS))
|
||||
}
|
||||
|
||||
pub fn new_with_timeout(http_endpoint: String, timeout: Duration) -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
http: HttpClient::new(http_endpoint, timeout)
|
||||
.map_err(|e| format!("Unable to create http client: {:?}", e))?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Unable to parse a URL. Check the server URL.
|
||||
UrlParseError(url::ParseError),
|
||||
/// The `reqwest` library returned an error.
|
||||
ReqwestError(reqwest::Error),
|
||||
/// There was an error when encoding/decoding an object using serde.
|
||||
SerdeJsonError(serde_json::Error),
|
||||
/// The server responded to the request, however it did not return a 200-type success code.
|
||||
DidNotSucceed { status: StatusCode, body: String },
|
||||
/// The request input was invalid.
|
||||
InvalidInput,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HttpClient<E> {
|
||||
client: Client,
|
||||
url: Url,
|
||||
timeout: Duration,
|
||||
_phantom: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<E: EthSpec> HttpClient<E> {
|
||||
/// Creates a new instance (without connecting to the node).
|
||||
pub fn new(server_url: String, timeout: Duration) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
client: ClientBuilder::new()
|
||||
.timeout(timeout)
|
||||
.build()
|
||||
.expect("should build from static configuration"),
|
||||
url: Url::parse(&server_url)?,
|
||||
timeout: Duration::from_secs(15),
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn beacon(&self) -> Beacon<E> {
|
||||
Beacon(self.clone())
|
||||
}
|
||||
|
||||
pub fn validator(&self) -> Validator<E> {
|
||||
Validator(self.clone())
|
||||
}
|
||||
|
||||
pub fn spec(&self) -> Spec<E> {
|
||||
Spec(self.clone())
|
||||
}
|
||||
|
||||
pub fn node(&self) -> Node<E> {
|
||||
Node(self.clone())
|
||||
}
|
||||
|
||||
pub fn advanced(&self) -> Advanced<E> {
|
||||
Advanced(self.clone())
|
||||
}
|
||||
|
||||
pub fn consensus(&self) -> Consensus<E> {
|
||||
Consensus(self.clone())
|
||||
}
|
||||
|
||||
fn url(&self, path: &str) -> Result<Url, Error> {
|
||||
self.url.join(path).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
pub async fn json_post<T: Serialize>(&self, url: Url, body: T) -> Result<Response, Error> {
|
||||
self.client
|
||||
.post(&url.to_string())
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub async fn json_get<T: DeserializeOwned>(
|
||||
&self,
|
||||
mut url: Url,
|
||||
query_pairs: Vec<(String, String)>,
|
||||
) -> Result<T, Error> {
|
||||
query_pairs.into_iter().for_each(|(key, param)| {
|
||||
url.query_pairs_mut().append_pair(&key, ¶m);
|
||||
});
|
||||
|
||||
let response = self
|
||||
.client
|
||||
.get(&url.to_string())
|
||||
.send()
|
||||
.await
|
||||
.map_err(Error::from)?;
|
||||
|
||||
let success = error_for_status(response).await.map_err(Error::from)?;
|
||||
success.json::<T>().await.map_err(Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an `Error` (with a description) if the `response` was not a 200-type success response.
|
||||
///
|
||||
/// Distinct from `Response::error_for_status` because it includes the body of the response as
|
||||
/// text. This ensures the error message from the server is not discarded.
|
||||
async fn error_for_status(response: Response) -> Result<Response, Error> {
|
||||
let status = response.status();
|
||||
|
||||
if status.is_success() {
|
||||
return Ok(response);
|
||||
} else {
|
||||
let text_result = response.text().await;
|
||||
match text_result {
|
||||
Err(e) => Err(Error::ReqwestError(e)),
|
||||
Ok(body) => Err(Error::DidNotSucceed { status, body }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum PublishStatus {
|
||||
/// The object was valid and has been published to the network.
|
||||
Valid,
|
||||
/// The object was not valid and may or may not have been published to the network.
|
||||
Invalid(String),
|
||||
/// The server responded with an unknown status code. The object may or may not have been
|
||||
/// published to the network.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl PublishStatus {
|
||||
/// Returns `true` if `*self == PublishStatus::Valid`.
|
||||
pub fn is_valid(&self) -> bool {
|
||||
*self == PublishStatus::Valid
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the functions on the `/validator` endpoint of the node.
|
||||
#[derive(Clone)]
|
||||
pub struct Validator<E>(HttpClient<E>);
|
||||
|
||||
impl<E: EthSpec> Validator<E> {
|
||||
fn url(&self, path: &str) -> Result<Url, Error> {
|
||||
self.0
|
||||
.url("validator/")
|
||||
.and_then(move |url| url.join(path).map_err(Error::from))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Produces an unsigned attestation.
|
||||
pub async fn produce_attestation(
|
||||
&self,
|
||||
slot: Slot,
|
||||
committee_index: CommitteeIndex,
|
||||
) -> Result<Attestation<E>, Error> {
|
||||
let query_params = vec![
|
||||
("slot".into(), format!("{}", slot)),
|
||||
("committee_index".into(), format!("{}", committee_index)),
|
||||
];
|
||||
|
||||
let client = self.0.clone();
|
||||
let url = self.url("attestation")?;
|
||||
client.json_get(url, query_params).await
|
||||
}
|
||||
|
||||
/// Produces an aggregate attestation.
|
||||
pub async fn produce_aggregate_attestation(
|
||||
&self,
|
||||
attestation_data: &AttestationData,
|
||||
) -> Result<Attestation<E>, Error> {
|
||||
let query_params = vec![(
|
||||
"attestation_data".into(),
|
||||
as_ssz_hex_string(attestation_data),
|
||||
)];
|
||||
|
||||
let client = self.0.clone();
|
||||
let url = self.url("aggregate_attestation")?;
|
||||
client.json_get(url, query_params).await
|
||||
}
|
||||
|
||||
/// Posts a list of attestations to the beacon node, expecting it to verify it and publish it to the network.
|
||||
pub async fn publish_attestations(
|
||||
&self,
|
||||
attestation: Vec<Attestation<E>>,
|
||||
) -> Result<PublishStatus, Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("attestations")?;
|
||||
let response = client.json_post::<_>(url, attestation).await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => Ok(PublishStatus::Valid),
|
||||
StatusCode::ACCEPTED => Ok(PublishStatus::Invalid(
|
||||
response.text().await.map_err(Error::from)?,
|
||||
)),
|
||||
_ => response
|
||||
.error_for_status()
|
||||
.map_err(Error::from)
|
||||
.map(|_| PublishStatus::Unknown),
|
||||
}
|
||||
}
|
||||
|
||||
/// Posts a list of signed aggregates and proofs to the beacon node, expecting it to verify it and publish it to the network.
|
||||
pub async fn publish_aggregate_and_proof(
|
||||
&self,
|
||||
signed_aggregate_and_proofs: Vec<SignedAggregateAndProof<E>>,
|
||||
) -> Result<PublishStatus, Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("aggregate_and_proofs")?;
|
||||
let response = client
|
||||
.json_post::<_>(url, signed_aggregate_and_proofs)
|
||||
.await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => Ok(PublishStatus::Valid),
|
||||
StatusCode::ACCEPTED => Ok(PublishStatus::Invalid(
|
||||
response.text().await.map_err(Error::from)?,
|
||||
)),
|
||||
_ => response
|
||||
.error_for_status()
|
||||
.map_err(Error::from)
|
||||
.map(|_| PublishStatus::Unknown),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the duties required of the given validator pubkeys in the given epoch.
|
||||
pub async fn get_duties(
|
||||
&self,
|
||||
epoch: Epoch,
|
||||
validator_pubkeys: &[PublicKey],
|
||||
) -> Result<Vec<ValidatorDutyBytes>, Error> {
|
||||
let client = self.0.clone();
|
||||
|
||||
let bulk_request = ValidatorDutiesRequest {
|
||||
epoch,
|
||||
pubkeys: validator_pubkeys
|
||||
.iter()
|
||||
.map(|pubkey| pubkey.clone().into())
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let url = self.url("duties")?;
|
||||
let response = client.json_post::<_>(url, bulk_request).await?;
|
||||
let success = error_for_status(response).await.map_err(Error::from)?;
|
||||
success.json().await.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Posts a block to the beacon node, expecting it to verify it and publish it to the network.
|
||||
pub async fn publish_block(&self, block: SignedBeaconBlock<E>) -> Result<PublishStatus, Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("block")?;
|
||||
let response = client.json_post::<_>(url, block).await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => Ok(PublishStatus::Valid),
|
||||
StatusCode::ACCEPTED => Ok(PublishStatus::Invalid(
|
||||
response.text().await.map_err(Error::from)?,
|
||||
)),
|
||||
_ => response
|
||||
.error_for_status()
|
||||
.map_err(Error::from)
|
||||
.map(|_| PublishStatus::Unknown),
|
||||
}
|
||||
}
|
||||
|
||||
/// Requests a new (unsigned) block from the beacon node.
|
||||
pub async fn produce_block(
|
||||
&self,
|
||||
slot: Slot,
|
||||
randao_reveal: Signature,
|
||||
) -> Result<BeaconBlock<E>, Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("block")?;
|
||||
client
|
||||
.json_get::<BeaconBlock<E>>(
|
||||
url,
|
||||
vec![
|
||||
("slot".into(), format!("{}", slot.as_u64())),
|
||||
("randao_reveal".into(), as_ssz_hex_string(&randao_reveal)),
|
||||
],
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Subscribes a list of validators to particular slots for attestation production/publication.
|
||||
pub async fn subscribe(
|
||||
&self,
|
||||
subscriptions: Vec<ValidatorSubscription>,
|
||||
) -> Result<PublishStatus, Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("subscribe")?;
|
||||
let response = client.json_post::<_>(url, subscriptions).await?;
|
||||
|
||||
match response.status() {
|
||||
StatusCode::OK => Ok(PublishStatus::Valid),
|
||||
StatusCode::ACCEPTED => Ok(PublishStatus::Invalid(
|
||||
response.text().await.map_err(Error::from)?,
|
||||
)),
|
||||
_ => response
|
||||
.error_for_status()
|
||||
.map_err(Error::from)
|
||||
.map(|_| PublishStatus::Unknown),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the functions on the `/beacon` endpoint of the node.
|
||||
#[derive(Clone)]
|
||||
pub struct Beacon<E>(HttpClient<E>);
|
||||
|
||||
impl<E: EthSpec> Beacon<E> {
|
||||
fn url(&self, path: &str) -> Result<Url, Error> {
|
||||
self.0
|
||||
.url("beacon/")
|
||||
.and_then(move |url| url.join(path).map_err(Error::from))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the genesis time.
|
||||
pub async fn get_genesis_time(&self) -> Result<u64, Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("genesis_time")?;
|
||||
client.json_get(url, vec![]).await
|
||||
}
|
||||
|
||||
/// Returns the genesis validators root.
|
||||
pub async fn get_genesis_validators_root(&self) -> Result<Hash256, Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("genesis_validators_root")?;
|
||||
client.json_get(url, vec![]).await
|
||||
}
|
||||
|
||||
/// Returns the fork at the head of the beacon chain.
|
||||
pub async fn get_fork(&self) -> Result<Fork, Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("fork")?;
|
||||
client.json_get(url, vec![]).await
|
||||
}
|
||||
|
||||
/// Returns info about the head of the canonical beacon chain.
|
||||
pub async fn get_head(&self) -> Result<CanonicalHeadResponse, Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("head")?;
|
||||
client.json_get::<CanonicalHeadResponse>(url, vec![]).await
|
||||
}
|
||||
|
||||
/// Returns the set of known beacon chain head blocks. One of these will be the canonical head.
|
||||
pub async fn get_heads(&self) -> Result<Vec<HeadBeaconBlock>, Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("heads")?;
|
||||
client.json_get(url, vec![]).await
|
||||
}
|
||||
|
||||
/// Returns the block and block root at the given slot.
|
||||
pub async fn get_block_by_slot(
|
||||
&self,
|
||||
slot: Slot,
|
||||
) -> Result<(SignedBeaconBlock<E>, Hash256), Error> {
|
||||
self.get_block("slot".to_string(), format!("{}", slot.as_u64()))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns the block and block root at the given root.
|
||||
pub async fn get_block_by_root(
|
||||
&self,
|
||||
root: Hash256,
|
||||
) -> Result<(SignedBeaconBlock<E>, Hash256), Error> {
|
||||
self.get_block("root".to_string(), root_as_string(root))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns the block and block root at the given slot.
|
||||
async fn get_block(
|
||||
&self,
|
||||
query_key: String,
|
||||
query_param: String,
|
||||
) -> Result<(SignedBeaconBlock<E>, Hash256), Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("block")?;
|
||||
client
|
||||
.json_get::<BlockResponse<E>>(url, vec![(query_key, query_param)])
|
||||
.await
|
||||
.map(|response| (response.beacon_block, response.root))
|
||||
}
|
||||
|
||||
/// Returns the state and state root at the given slot.
|
||||
pub async fn get_state_by_slot(&self, slot: Slot) -> Result<(BeaconState<E>, Hash256), Error> {
|
||||
self.get_state("slot".to_string(), format!("{}", slot.as_u64()))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns the state and state root at the given root.
|
||||
pub async fn get_state_by_root(
|
||||
&self,
|
||||
root: Hash256,
|
||||
) -> Result<(BeaconState<E>, Hash256), Error> {
|
||||
self.get_state("root".to_string(), root_as_string(root))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns the root of the state at the given slot.
|
||||
pub async fn get_state_root(&self, slot: Slot) -> Result<Hash256, Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("state_root")?;
|
||||
client
|
||||
.json_get(url, vec![("slot".into(), format!("{}", slot.as_u64()))])
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns the root of the block at the given slot.
|
||||
pub async fn get_block_root(&self, slot: Slot) -> Result<Hash256, Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("block_root")?;
|
||||
client
|
||||
.json_get(url, vec![("slot".into(), format!("{}", slot.as_u64()))])
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns the state and state root at the given slot.
|
||||
async fn get_state(
|
||||
&self,
|
||||
query_key: String,
|
||||
query_param: String,
|
||||
) -> Result<(BeaconState<E>, Hash256), Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("state")?;
|
||||
client
|
||||
.json_get::<StateResponse<E>>(url, vec![(query_key, query_param)])
|
||||
.await
|
||||
.map(|response| (response.beacon_state, response.root))
|
||||
}
|
||||
|
||||
/// Returns the block and block root at the given slot.
|
||||
///
|
||||
/// If `state_root` is `Some`, the query will use the given state instead of the default
|
||||
/// canonical head state.
|
||||
pub async fn get_validators(
|
||||
&self,
|
||||
validator_pubkeys: Vec<PublicKey>,
|
||||
state_root: Option<Hash256>,
|
||||
) -> Result<Vec<ValidatorResponse>, Error> {
|
||||
let client = self.0.clone();
|
||||
|
||||
let bulk_request = ValidatorRequest {
|
||||
state_root,
|
||||
pubkeys: validator_pubkeys
|
||||
.iter()
|
||||
.map(|pubkey| pubkey.clone().into())
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let url = self.url("validators")?;
|
||||
let response = client.json_post::<_>(url, bulk_request).await?;
|
||||
let success = error_for_status(response).await.map_err(Error::from)?;
|
||||
success.json().await.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Returns all validators.
|
||||
///
|
||||
/// If `state_root` is `Some`, the query will use the given state instead of the default
|
||||
/// canonical head state.
|
||||
pub async fn get_all_validators(
|
||||
&self,
|
||||
state_root: Option<Hash256>,
|
||||
) -> Result<Vec<ValidatorResponse>, Error> {
|
||||
let client = self.0.clone();
|
||||
|
||||
let query_params = if let Some(state_root) = state_root {
|
||||
vec![("state_root".into(), root_as_string(state_root))]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
let url = self.url("validators/all")?;
|
||||
client.json_get(url, query_params).await
|
||||
}
|
||||
|
||||
/// Returns the active validators.
|
||||
///
|
||||
/// If `state_root` is `Some`, the query will use the given state instead of the default
|
||||
/// canonical head state.
|
||||
pub async fn get_active_validators(
|
||||
&self,
|
||||
state_root: Option<Hash256>,
|
||||
) -> Result<Vec<ValidatorResponse>, Error> {
|
||||
let client = self.0.clone();
|
||||
|
||||
let query_params = if let Some(state_root) = state_root {
|
||||
vec![("state_root".into(), root_as_string(state_root))]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
let url = self.url("validators/active")?;
|
||||
client.json_get(url, query_params).await
|
||||
}
|
||||
|
||||
/// Returns committees at the given epoch.
|
||||
pub async fn get_committees(&self, epoch: Epoch) -> Result<Vec<Committee>, Error> {
|
||||
let client = self.0.clone();
|
||||
|
||||
let url = self.url("committees")?;
|
||||
client
|
||||
.json_get(url, vec![("epoch".into(), format!("{}", epoch.as_u64()))])
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn proposer_slashing(
|
||||
&self,
|
||||
proposer_slashing: ProposerSlashing,
|
||||
) -> Result<bool, Error> {
|
||||
let client = self.0.clone();
|
||||
|
||||
let url = self.url("proposer_slashing")?;
|
||||
let response = client.json_post::<_>(url, proposer_slashing).await?;
|
||||
let success = error_for_status(response).await.map_err(Error::from)?;
|
||||
success.json().await.map_err(Error::from)
|
||||
}
|
||||
|
||||
pub async fn attester_slashing(
|
||||
&self,
|
||||
attester_slashing: AttesterSlashing<E>,
|
||||
) -> Result<bool, Error> {
|
||||
let client = self.0.clone();
|
||||
|
||||
let url = self.url("attester_slashing")?;
|
||||
let response = client.json_post::<_>(url, attester_slashing).await?;
|
||||
let success = error_for_status(response).await.map_err(Error::from)?;
|
||||
success.json().await.map_err(Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the functions on the `/spec` endpoint of the node.
|
||||
#[derive(Clone)]
|
||||
pub struct Spec<E>(HttpClient<E>);
|
||||
|
||||
impl<E: EthSpec> Spec<E> {
|
||||
fn url(&self, path: &str) -> Result<Url, Error> {
|
||||
self.0
|
||||
.url("spec/")
|
||||
.and_then(move |url| url.join(path).map_err(Error::from))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn get_eth2_config(&self) -> Result<Eth2Config, Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("eth2_config")?;
|
||||
client.json_get(url, vec![]).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the functions on the `/node` endpoint of the node.
|
||||
#[derive(Clone)]
|
||||
pub struct Node<E>(HttpClient<E>);
|
||||
|
||||
impl<E: EthSpec> Node<E> {
|
||||
fn url(&self, path: &str) -> Result<Url, Error> {
|
||||
self.0
|
||||
.url("node/")
|
||||
.and_then(move |url| url.join(path).map_err(Error::from))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn get_version(&self) -> Result<String, Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("version")?;
|
||||
client.json_get(url, vec![]).await
|
||||
}
|
||||
|
||||
pub async fn syncing_status(&self) -> Result<SyncingResponse, Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("syncing")?;
|
||||
client.json_get(url, vec![]).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the functions on the `/advanced` endpoint of the node.
|
||||
#[derive(Clone)]
|
||||
pub struct Advanced<E>(HttpClient<E>);
|
||||
|
||||
impl<E: EthSpec> Advanced<E> {
|
||||
fn url(&self, path: &str) -> Result<Url, Error> {
|
||||
self.0
|
||||
.url("advanced/")
|
||||
.and_then(move |url| url.join(path).map_err(Error::from))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Gets the core `ProtoArray` struct from the node.
|
||||
pub async fn get_fork_choice(&self) -> Result<ProtoArray, Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("fork_choice")?;
|
||||
client.json_get(url, vec![]).await
|
||||
}
|
||||
|
||||
/// Gets the core `PersistedOperationPool` struct from the node.
|
||||
pub async fn get_operation_pool(&self) -> Result<PersistedOperationPool<E>, Error> {
|
||||
let client = self.0.clone();
|
||||
let url = self.url("operation_pool")?;
|
||||
client.json_get(url, vec![]).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the functions on the `/consensus` endpoint of the node.
|
||||
#[derive(Clone)]
|
||||
pub struct Consensus<E>(HttpClient<E>);
|
||||
|
||||
impl<E: EthSpec> Consensus<E> {
|
||||
fn url(&self, path: &str) -> Result<Url, Error> {
|
||||
self.0
|
||||
.url("consensus/")
|
||||
.and_then(move |url| url.join(path).map_err(Error::from))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Gets a `IndividualVote` for each of the given `pubkeys`.
|
||||
pub async fn get_individual_votes(
|
||||
&self,
|
||||
epoch: Epoch,
|
||||
pubkeys: Vec<PublicKeyBytes>,
|
||||
) -> Result<IndividualVotesResponse, Error> {
|
||||
let client = self.0.clone();
|
||||
let req_body = IndividualVotesRequest { epoch, pubkeys };
|
||||
|
||||
let url = self.url("individual_votes")?;
|
||||
let response = client.json_post::<_>(url, req_body).await?;
|
||||
let success = error_for_status(response).await.map_err(Error::from)?;
|
||||
success.json().await.map_err(Error::from)
|
||||
}
|
||||
|
||||
/// Gets a `VoteCount` for the given `epoch`.
|
||||
pub async fn get_vote_count(&self, epoch: Epoch) -> Result<IndividualVotesResponse, Error> {
|
||||
let client = self.0.clone();
|
||||
let query_params = vec![("epoch".into(), format!("{}", epoch.as_u64()))];
|
||||
let url = self.url("vote_count")?;
|
||||
client.json_get(url, query_params).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(bound = "T: EthSpec")]
|
||||
pub struct BlockResponse<T: EthSpec> {
|
||||
pub beacon_block: SignedBeaconBlock<T>,
|
||||
pub root: Hash256,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(bound = "T: EthSpec")]
|
||||
pub struct StateResponse<T: EthSpec> {
|
||||
pub beacon_state: BeaconState<T>,
|
||||
pub root: Hash256,
|
||||
}
|
||||
|
||||
fn root_as_string(root: Hash256) -> String {
|
||||
format!("0x{:?}", root)
|
||||
}
|
||||
|
||||
fn as_ssz_hex_string<T: Encode>(item: &T) -> String {
|
||||
format!("0x{}", hex::encode(item.as_ssz_bytes()))
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(e: reqwest::Error) -> Error {
|
||||
Error::ReqwestError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for Error {
|
||||
fn from(e: url::ParseError) -> Error {
|
||||
Error::UrlParseError(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(e: serde_json::Error) -> Error {
|
||||
Error::SerdeJsonError(e)
|
||||
}
|
||||
}
|
||||
16
common/rest_types/Cargo.toml
Normal file
16
common/rest_types/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "rest_types"
|
||||
version = "0.2.0"
|
||||
authors = ["Age Manning <Age@AgeManning.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
types = { path = "../../consensus/types" }
|
||||
eth2_ssz_derive = "0.1.0"
|
||||
eth2_ssz = "0.1.2"
|
||||
eth2_hashing = "0.1.0"
|
||||
tree_hash = "0.1.0"
|
||||
state_processing = { path = "../../consensus/state_processing" }
|
||||
bls = { path = "../../crypto/bls" }
|
||||
serde = { version = "1.0.110", features = ["derive"] }
|
||||
rayon = "1.3.0"
|
||||
65
common/rest_types/src/beacon.rs
Normal file
65
common/rest_types/src/beacon.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
//! A collection of REST API types for interaction with the beacon node.
|
||||
|
||||
use bls::PublicKeyBytes;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use types::beacon_state::EthSpec;
|
||||
use types::{BeaconState, CommitteeIndex, Hash256, SignedBeaconBlock, Slot, Validator};
|
||||
|
||||
/// Information about a block that is at the head of a chain. May or may not represent the
|
||||
/// canonical head.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)]
|
||||
pub struct HeadBeaconBlock {
|
||||
pub beacon_block_root: Hash256,
|
||||
pub beacon_block_slot: Slot,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)]
|
||||
#[serde(bound = "T: EthSpec")]
|
||||
pub struct BlockResponse<T: EthSpec> {
|
||||
pub root: Hash256,
|
||||
pub beacon_block: SignedBeaconBlock<T>,
|
||||
}
|
||||
|
||||
/// Information about the block and state that are at head of the beacon chain.
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)]
|
||||
pub struct CanonicalHeadResponse {
|
||||
pub slot: Slot,
|
||||
pub block_root: Hash256,
|
||||
pub state_root: Hash256,
|
||||
pub finalized_slot: Slot,
|
||||
pub finalized_block_root: Hash256,
|
||||
pub justified_slot: Slot,
|
||||
pub justified_block_root: Hash256,
|
||||
pub previous_justified_slot: Slot,
|
||||
pub previous_justified_block_root: Hash256,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)]
|
||||
pub struct ValidatorResponse {
|
||||
pub pubkey: PublicKeyBytes,
|
||||
pub validator_index: Option<usize>,
|
||||
pub balance: Option<u64>,
|
||||
pub validator: Option<Validator>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)]
|
||||
pub struct ValidatorRequest {
|
||||
/// If set to `None`, uses the canonical head state.
|
||||
pub state_root: Option<Hash256>,
|
||||
pub pubkeys: Vec<PublicKeyBytes>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)]
|
||||
pub struct Committee {
|
||||
pub slot: Slot,
|
||||
pub index: CommitteeIndex,
|
||||
pub committee: Vec<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)]
|
||||
#[serde(bound = "T: EthSpec")]
|
||||
pub struct StateResponse<T: EthSpec> {
|
||||
pub root: Hash256,
|
||||
pub beacon_state: BeaconState<T>,
|
||||
}
|
||||
66
common/rest_types/src/consensus.rs
Normal file
66
common/rest_types/src/consensus.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use state_processing::per_epoch_processing::ValidatorStatus;
|
||||
use types::{Epoch, PublicKeyBytes};
|
||||
|
||||
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)]
|
||||
pub struct IndividualVotesRequest {
|
||||
pub epoch: Epoch,
|
||||
pub pubkeys: Vec<PublicKeyBytes>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)]
|
||||
pub struct IndividualVote {
|
||||
/// True if the validator has been slashed, ever.
|
||||
pub is_slashed: bool,
|
||||
/// True if the validator can withdraw in the current epoch.
|
||||
pub is_withdrawable_in_current_epoch: bool,
|
||||
/// True if the validator was active in the state's _current_ epoch.
|
||||
pub is_active_in_current_epoch: bool,
|
||||
/// True if the validator was active in the state's _previous_ epoch.
|
||||
pub is_active_in_previous_epoch: bool,
|
||||
/// The validator's effective balance in the _current_ epoch.
|
||||
pub current_epoch_effective_balance_gwei: u64,
|
||||
/// True if the validator had an attestation included in the _current_ epoch.
|
||||
pub is_current_epoch_attester: bool,
|
||||
/// True if the validator's beacon block root attestation for the first slot of the _current_
|
||||
/// epoch matches the block root known to the state.
|
||||
pub is_current_epoch_target_attester: bool,
|
||||
/// True if the validator had an attestation included in the _previous_ epoch.
|
||||
pub is_previous_epoch_attester: bool,
|
||||
/// True if the validator's beacon block root attestation for the first slot of the _previous_
|
||||
/// epoch matches the block root known to the state.
|
||||
pub is_previous_epoch_target_attester: bool,
|
||||
/// True if the validator's beacon block root attestation in the _previous_ epoch at the
|
||||
/// attestation's slot (`attestation_data.slot`) matches the block root known to the state.
|
||||
pub is_previous_epoch_head_attester: bool,
|
||||
}
|
||||
|
||||
impl Into<IndividualVote> for ValidatorStatus {
|
||||
fn into(self) -> IndividualVote {
|
||||
IndividualVote {
|
||||
is_slashed: self.is_slashed,
|
||||
is_withdrawable_in_current_epoch: self.is_withdrawable_in_current_epoch,
|
||||
is_active_in_current_epoch: self.is_active_in_current_epoch,
|
||||
is_active_in_previous_epoch: self.is_active_in_previous_epoch,
|
||||
current_epoch_effective_balance_gwei: self.current_epoch_effective_balance,
|
||||
is_current_epoch_attester: self.is_current_epoch_attester,
|
||||
is_current_epoch_target_attester: self.is_current_epoch_target_attester,
|
||||
is_previous_epoch_attester: self.is_previous_epoch_attester,
|
||||
is_previous_epoch_target_attester: self.is_previous_epoch_target_attester,
|
||||
is_previous_epoch_head_attester: self.is_previous_epoch_head_attester,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)]
|
||||
pub struct IndividualVotesResponse {
|
||||
/// The epoch which is considered the "current" epoch.
|
||||
pub epoch: Epoch,
|
||||
/// The validators public key.
|
||||
pub pubkey: PublicKeyBytes,
|
||||
/// The index of the validator in state.validators.
|
||||
pub validator_index: Option<usize>,
|
||||
/// Voting statistics for the validator, if they voted in the given epoch.
|
||||
pub vote: Option<IndividualVote>,
|
||||
}
|
||||
21
common/rest_types/src/lib.rs
Normal file
21
common/rest_types/src/lib.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
//! A collection of types used to pass data across the rest HTTP API.
|
||||
//!
|
||||
//! This is primarily used by the validator client and the beacon node rest API.
|
||||
|
||||
mod beacon;
|
||||
mod consensus;
|
||||
mod node;
|
||||
mod validator;
|
||||
|
||||
pub use beacon::{
|
||||
BlockResponse, CanonicalHeadResponse, Committee, HeadBeaconBlock, StateResponse,
|
||||
ValidatorRequest, ValidatorResponse,
|
||||
};
|
||||
|
||||
pub use validator::{
|
||||
ValidatorDutiesRequest, ValidatorDuty, ValidatorDutyBytes, ValidatorSubscription,
|
||||
};
|
||||
|
||||
pub use consensus::{IndividualVote, IndividualVotesRequest, IndividualVotesResponse};
|
||||
|
||||
pub use node::{SyncingResponse, SyncingStatus};
|
||||
32
common/rest_types/src/node.rs
Normal file
32
common/rest_types/src/node.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
//! Collection of types for the /node HTTP
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use types::Slot;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)]
|
||||
/// The current syncing status of the node.
|
||||
pub struct SyncingStatus {
|
||||
/// The starting slot of sync.
|
||||
///
|
||||
/// For a finalized sync, this is the start slot of the current finalized syncing
|
||||
/// chain.
|
||||
///
|
||||
/// For head sync this is the last finalized slot.
|
||||
pub starting_slot: Slot,
|
||||
/// The current slot.
|
||||
pub current_slot: Slot,
|
||||
/// The highest known slot. For the current syncing chain.
|
||||
///
|
||||
/// For a finalized sync, the target finalized slot.
|
||||
/// For head sync, this is the highest known slot of all head chains.
|
||||
pub highest_slot: Slot,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)]
|
||||
/// The response for the /node/syncing HTTP GET.
|
||||
pub struct SyncingResponse {
|
||||
/// Is the node syncing.
|
||||
pub is_syncing: bool,
|
||||
/// The current sync status.
|
||||
pub sync_status: SyncingStatus,
|
||||
}
|
||||
72
common/rest_types/src/validator.rs
Normal file
72
common/rest_types/src/validator.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use bls::{PublicKey, PublicKeyBytes, Signature};
|
||||
use eth2_hashing::hash;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ssz_derive::{Decode, Encode};
|
||||
use std::convert::TryInto;
|
||||
use types::{CommitteeIndex, Epoch, Slot};
|
||||
|
||||
/// A Validator duty with the validator public key represented a `PublicKeyBytes`.
|
||||
pub type ValidatorDutyBytes = ValidatorDutyBase<PublicKeyBytes>;
|
||||
/// A validator duty with the pubkey represented as a `PublicKey`.
|
||||
pub type ValidatorDuty = ValidatorDutyBase<PublicKey>;
|
||||
|
||||
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ValidatorDutyBase<T> {
|
||||
/// The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._
|
||||
pub validator_pubkey: T,
|
||||
/// The validator's index in `state.validators`
|
||||
pub validator_index: Option<u64>,
|
||||
/// The slot at which the validator must attest.
|
||||
pub attestation_slot: Option<Slot>,
|
||||
/// The index of the committee within `slot` of which the validator is a member.
|
||||
pub attestation_committee_index: Option<CommitteeIndex>,
|
||||
/// The position of the validator in the committee.
|
||||
pub attestation_committee_position: Option<usize>,
|
||||
/// The slots in which a validator must propose a block (can be empty).
|
||||
pub block_proposal_slots: Vec<Slot>,
|
||||
/// This provides the modulo: `max(1, len(committee) // TARGET_AGGREGATORS_PER_COMMITTEE)`
|
||||
/// which allows the validator client to determine if this duty requires the validator to be
|
||||
/// aggregate attestations.
|
||||
pub aggregator_modulo: Option<u64>,
|
||||
}
|
||||
|
||||
impl<T> ValidatorDutyBase<T> {
|
||||
/// Given a `slot_signature` determines if the validator of this duty is an aggregator.
|
||||
// Note that we assume the signature is for the associated pubkey to avoid the signature
|
||||
// verification
|
||||
pub fn is_aggregator(&self, slot_signature: &Signature) -> bool {
|
||||
if let Some(modulo) = self.aggregator_modulo {
|
||||
let signature_hash = hash(&slot_signature.as_bytes());
|
||||
let signature_hash_int = u64::from_le_bytes(
|
||||
signature_hash[0..8]
|
||||
.try_into()
|
||||
.expect("first 8 bytes of signature should always convert to fixed array"),
|
||||
);
|
||||
signature_hash_int % modulo == 0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)]
|
||||
pub struct ValidatorDutiesRequest {
|
||||
pub epoch: Epoch,
|
||||
pub pubkeys: Vec<PublicKeyBytes>,
|
||||
}
|
||||
|
||||
/// A validator subscription, created when a validator subscribes to a slot to perform optional aggregation
|
||||
/// duties.
|
||||
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)]
|
||||
pub struct ValidatorSubscription {
|
||||
/// The validators index.
|
||||
pub validator_index: u64,
|
||||
/// The index of the committee within `slot` of which the validator is a member. Used by the
|
||||
/// beacon node to quickly evaluate the associated `SubnetId`.
|
||||
pub attestation_committee_index: CommitteeIndex,
|
||||
/// The slot in which to subscribe.
|
||||
pub slot: Slot,
|
||||
/// If true, the validator is an aggregator and the beacon node should aggregate attestations
|
||||
/// for this slot.
|
||||
pub is_aggregator: bool,
|
||||
}
|
||||
11
common/slot_clock/Cargo.toml
Normal file
11
common/slot_clock/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "slot_clock"
|
||||
version = "0.2.0"
|
||||
authors = ["Paul Hauner <paul@paulhauner.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
types = { path = "../../consensus/types" }
|
||||
lazy_static = "1.4.0"
|
||||
lighthouse_metrics = { path = "../lighthouse_metrics" }
|
||||
parking_lot = "0.10.2"
|
||||
60
common/slot_clock/src/lib.rs
Normal file
60
common/slot_clock/src/lib.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
|
||||
mod manual_slot_clock;
|
||||
mod metrics;
|
||||
mod system_time_slot_clock;
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
pub use crate::manual_slot_clock::ManualSlotClock;
|
||||
pub use crate::manual_slot_clock::ManualSlotClock as TestingSlotClock;
|
||||
pub use crate::system_time_slot_clock::SystemTimeSlotClock;
|
||||
pub use metrics::scrape_for_metrics;
|
||||
pub use types::Slot;
|
||||
|
||||
/// A clock that reports the current slot.
|
||||
///
|
||||
/// The clock is not required to be monotonically increasing and may go backwards.
|
||||
pub trait SlotClock: Send + Sync + Sized {
|
||||
/// Creates a new slot clock where the first slot is `genesis_slot`, genesis occurred
|
||||
/// `genesis_duration` after the `UNIX_EPOCH` and each slot is `slot_duration` apart.
|
||||
fn new(genesis_slot: Slot, genesis_duration: Duration, slot_duration: Duration) -> Self;
|
||||
|
||||
/// Returns the slot at this present time.
|
||||
fn now(&self) -> Option<Slot>;
|
||||
|
||||
/// Returns the present time as a duration since the UNIX epoch.
|
||||
///
|
||||
/// Returns `None` if the present time is before the UNIX epoch (unlikely).
|
||||
fn now_duration(&self) -> Option<Duration>;
|
||||
|
||||
/// Returns the slot of the given duration since the UNIX epoch.
|
||||
fn slot_of(&self, now: Duration) -> Option<Slot>;
|
||||
|
||||
/// Returns the duration between slots
|
||||
fn slot_duration(&self) -> Duration;
|
||||
|
||||
/// Returns the duration from now until `slot`.
|
||||
fn duration_to_slot(&self, slot: Slot) -> Option<Duration>;
|
||||
|
||||
/// Returns the duration until the next slot.
|
||||
fn duration_to_next_slot(&self) -> Option<Duration>;
|
||||
|
||||
/// Returns the duration until the first slot of the next epoch.
|
||||
fn duration_to_next_epoch(&self, slots_per_epoch: u64) -> Option<Duration>;
|
||||
|
||||
/// Returns the first slot to be returned at the genesis time.
|
||||
fn genesis_slot(&self) -> Slot;
|
||||
|
||||
/// Returns the slot if the internal clock were advanced by `duration`.
|
||||
fn now_with_future_tolerance(&self, tolerance: Duration) -> Option<Slot> {
|
||||
self.slot_of(self.now_duration()?.checked_add(tolerance)?)
|
||||
}
|
||||
|
||||
/// Returns the slot if the internal clock were reversed by `duration`.
|
||||
fn now_with_past_tolerance(&self, tolerance: Duration) -> Option<Slot> {
|
||||
self.slot_of(self.now_duration()?.checked_sub(tolerance)?)
|
||||
.or_else(|| Some(self.genesis_slot()))
|
||||
}
|
||||
}
|
||||
329
common/slot_clock/src/manual_slot_clock.rs
Normal file
329
common/slot_clock/src/manual_slot_clock.rs
Normal file
@@ -0,0 +1,329 @@
|
||||
use super::SlotClock;
|
||||
use parking_lot::RwLock;
|
||||
use std::convert::TryInto;
|
||||
use std::time::Duration;
|
||||
use types::Slot;
|
||||
|
||||
/// Determines the present slot based upon a manually-incremented UNIX timestamp.
|
||||
pub struct ManualSlotClock {
|
||||
genesis_slot: Slot,
|
||||
/// Duration from UNIX epoch to genesis.
|
||||
genesis_duration: Duration,
|
||||
/// Duration from UNIX epoch to right now.
|
||||
current_time: RwLock<Duration>,
|
||||
/// The length of each slot.
|
||||
slot_duration: Duration,
|
||||
}
|
||||
|
||||
impl Clone for ManualSlotClock {
|
||||
fn clone(&self) -> Self {
|
||||
ManualSlotClock {
|
||||
genesis_slot: self.genesis_slot.clone(),
|
||||
genesis_duration: self.genesis_duration.clone(),
|
||||
current_time: RwLock::new(self.current_time.read().clone()),
|
||||
slot_duration: self.slot_duration.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ManualSlotClock {
|
||||
pub fn set_slot(&self, slot: u64) {
|
||||
let slots_since_genesis = slot
|
||||
.checked_sub(self.genesis_slot.as_u64())
|
||||
.expect("slot must be post-genesis")
|
||||
.try_into()
|
||||
.expect("slot must fit within a u32");
|
||||
*self.current_time.write() =
|
||||
self.genesis_duration + self.slot_duration * slots_since_genesis;
|
||||
}
|
||||
|
||||
pub fn advance_slot(&self) {
|
||||
self.set_slot(self.now().unwrap().as_u64() + 1)
|
||||
}
|
||||
|
||||
/// Returns the duration between UNIX epoch and the start of `slot`.
|
||||
pub fn start_of(&self, slot: Slot) -> Option<Duration> {
|
||||
let slot = slot
|
||||
.as_u64()
|
||||
.checked_sub(self.genesis_slot.as_u64())?
|
||||
.try_into()
|
||||
.ok()?;
|
||||
let unadjusted_slot_duration = self.slot_duration.checked_mul(slot)?;
|
||||
|
||||
self.genesis_duration.checked_add(unadjusted_slot_duration)
|
||||
}
|
||||
|
||||
/// Returns the duration from `now` until the start of `slot`.
|
||||
///
|
||||
/// Will return `None` if `now` is later than the start of `slot`.
|
||||
pub fn duration_to_slot(&self, slot: Slot, now: Duration) -> Option<Duration> {
|
||||
self.start_of(slot)?.checked_sub(now)
|
||||
}
|
||||
|
||||
/// Returns the duration between `now` and the start of the next slot.
|
||||
pub fn duration_to_next_slot_from(&self, now: Duration) -> Option<Duration> {
|
||||
if now < self.genesis_duration {
|
||||
self.genesis_duration.checked_sub(now)
|
||||
} else {
|
||||
self.duration_to_slot(self.slot_of(now)? + 1, now)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the duration between `now` and the start of the next epoch.
|
||||
pub fn duration_to_next_epoch_from(
|
||||
&self,
|
||||
now: Duration,
|
||||
slots_per_epoch: u64,
|
||||
) -> Option<Duration> {
|
||||
if now < self.genesis_duration {
|
||||
self.genesis_duration.checked_sub(now)
|
||||
} else {
|
||||
let next_epoch_start_slot =
|
||||
(self.slot_of(now)?.epoch(slots_per_epoch) + 1).start_slot(slots_per_epoch);
|
||||
|
||||
self.duration_to_slot(next_epoch_start_slot, now)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SlotClock for ManualSlotClock {
|
||||
fn new(genesis_slot: Slot, genesis_duration: Duration, slot_duration: Duration) -> Self {
|
||||
if slot_duration.as_millis() == 0 {
|
||||
panic!("ManualSlotClock cannot have a < 1ms slot duration");
|
||||
}
|
||||
|
||||
Self {
|
||||
genesis_slot,
|
||||
current_time: RwLock::new(genesis_duration.clone()),
|
||||
genesis_duration,
|
||||
slot_duration,
|
||||
}
|
||||
}
|
||||
|
||||
fn now(&self) -> Option<Slot> {
|
||||
self.slot_of(*self.current_time.read())
|
||||
}
|
||||
|
||||
fn now_duration(&self) -> Option<Duration> {
|
||||
Some(*self.current_time.read())
|
||||
}
|
||||
|
||||
fn slot_of(&self, now: Duration) -> Option<Slot> {
|
||||
let genesis = self.genesis_duration;
|
||||
|
||||
if now >= genesis {
|
||||
let since_genesis = now
|
||||
.checked_sub(genesis)
|
||||
.expect("Control flow ensures now is greater than or equal to genesis");
|
||||
let slot =
|
||||
Slot::from((since_genesis.as_millis() / self.slot_duration.as_millis()) as u64);
|
||||
Some(slot + self.genesis_slot)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn duration_to_next_slot(&self) -> Option<Duration> {
|
||||
self.duration_to_next_slot_from(*self.current_time.read())
|
||||
}
|
||||
|
||||
fn duration_to_next_epoch(&self, slots_per_epoch: u64) -> Option<Duration> {
|
||||
self.duration_to_next_epoch_from(*self.current_time.read(), slots_per_epoch)
|
||||
}
|
||||
|
||||
fn slot_duration(&self) -> Duration {
|
||||
self.slot_duration
|
||||
}
|
||||
|
||||
fn duration_to_slot(&self, slot: Slot) -> Option<Duration> {
|
||||
self.duration_to_slot(slot, *self.current_time.read())
|
||||
}
|
||||
|
||||
fn genesis_slot(&self) -> Slot {
|
||||
self.genesis_slot
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_slot_now() {
|
||||
let clock = ManualSlotClock::new(
|
||||
Slot::new(10),
|
||||
Duration::from_secs(0),
|
||||
Duration::from_secs(1),
|
||||
);
|
||||
assert_eq!(clock.now(), Some(Slot::new(10)));
|
||||
clock.set_slot(123);
|
||||
assert_eq!(clock.now(), Some(Slot::new(123)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_of() {
|
||||
// Genesis slot and genesis duration 0.
|
||||
let clock =
|
||||
ManualSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(1));
|
||||
assert_eq!(clock.start_of(Slot::new(0)), Some(Duration::from_secs(0)));
|
||||
assert_eq!(clock.start_of(Slot::new(1)), Some(Duration::from_secs(1)));
|
||||
assert_eq!(clock.start_of(Slot::new(2)), Some(Duration::from_secs(2)));
|
||||
|
||||
// Genesis slot 1 and genesis duration 10.
|
||||
let clock = ManualSlotClock::new(
|
||||
Slot::new(0),
|
||||
Duration::from_secs(10),
|
||||
Duration::from_secs(1),
|
||||
);
|
||||
assert_eq!(clock.start_of(Slot::new(0)), Some(Duration::from_secs(10)));
|
||||
assert_eq!(clock.start_of(Slot::new(1)), Some(Duration::from_secs(11)));
|
||||
assert_eq!(clock.start_of(Slot::new(2)), Some(Duration::from_secs(12)));
|
||||
|
||||
// Genesis slot 1 and genesis duration 0.
|
||||
let clock =
|
||||
ManualSlotClock::new(Slot::new(1), Duration::from_secs(0), Duration::from_secs(1));
|
||||
assert_eq!(clock.start_of(Slot::new(0)), None);
|
||||
assert_eq!(clock.start_of(Slot::new(1)), Some(Duration::from_secs(0)));
|
||||
assert_eq!(clock.start_of(Slot::new(2)), Some(Duration::from_secs(1)));
|
||||
|
||||
// Genesis slot 1 and genesis duration 10.
|
||||
let clock = ManualSlotClock::new(
|
||||
Slot::new(1),
|
||||
Duration::from_secs(10),
|
||||
Duration::from_secs(1),
|
||||
);
|
||||
assert_eq!(clock.start_of(Slot::new(0)), None);
|
||||
assert_eq!(clock.start_of(Slot::new(1)), Some(Duration::from_secs(10)));
|
||||
assert_eq!(clock.start_of(Slot::new(2)), Some(Duration::from_secs(11)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_to_next_slot() {
|
||||
let slot_duration = Duration::from_secs(1);
|
||||
|
||||
// Genesis time is now.
|
||||
let clock = ManualSlotClock::new(Slot::new(0), Duration::from_secs(0), slot_duration);
|
||||
*clock.current_time.write() = Duration::from_secs(0);
|
||||
assert_eq!(clock.duration_to_next_slot(), Some(Duration::from_secs(1)));
|
||||
|
||||
// Genesis time is in the future.
|
||||
let clock = ManualSlotClock::new(Slot::new(0), Duration::from_secs(10), slot_duration);
|
||||
*clock.current_time.write() = Duration::from_secs(0);
|
||||
assert_eq!(clock.duration_to_next_slot(), Some(Duration::from_secs(10)));
|
||||
|
||||
// Genesis time is in the past.
|
||||
let clock = ManualSlotClock::new(Slot::new(0), Duration::from_secs(0), slot_duration);
|
||||
*clock.current_time.write() = Duration::from_secs(10);
|
||||
assert_eq!(clock.duration_to_next_slot(), Some(Duration::from_secs(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_to_next_epoch() {
|
||||
let slot_duration = Duration::from_secs(1);
|
||||
let slots_per_epoch = 32;
|
||||
|
||||
// Genesis time is now.
|
||||
let clock = ManualSlotClock::new(Slot::new(0), Duration::from_secs(0), slot_duration);
|
||||
*clock.current_time.write() = Duration::from_secs(0);
|
||||
assert_eq!(
|
||||
clock.duration_to_next_epoch(slots_per_epoch),
|
||||
Some(Duration::from_secs(32))
|
||||
);
|
||||
|
||||
// Genesis time is in the future.
|
||||
let clock = ManualSlotClock::new(Slot::new(0), Duration::from_secs(10), slot_duration);
|
||||
*clock.current_time.write() = Duration::from_secs(0);
|
||||
assert_eq!(
|
||||
clock.duration_to_next_epoch(slots_per_epoch),
|
||||
Some(Duration::from_secs(10))
|
||||
);
|
||||
|
||||
// Genesis time is in the past.
|
||||
let clock = ManualSlotClock::new(Slot::new(0), Duration::from_secs(0), slot_duration);
|
||||
*clock.current_time.write() = Duration::from_secs(10);
|
||||
assert_eq!(
|
||||
clock.duration_to_next_epoch(slots_per_epoch),
|
||||
Some(Duration::from_secs(22))
|
||||
);
|
||||
|
||||
// Genesis time is in the past.
|
||||
let clock = ManualSlotClock::new(
|
||||
Slot::new(0),
|
||||
Duration::from_secs(0),
|
||||
Duration::from_secs(12),
|
||||
);
|
||||
*clock.current_time.write() = Duration::from_secs(72_333);
|
||||
assert!(clock.duration_to_next_epoch(slots_per_epoch).is_some(),);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tolerance() {
|
||||
let clock = ManualSlotClock::new(
|
||||
Slot::new(0),
|
||||
Duration::from_secs(10),
|
||||
Duration::from_secs(1),
|
||||
);
|
||||
|
||||
// Set clock to the 0'th slot.
|
||||
*clock.current_time.write() = Duration::from_secs(10);
|
||||
assert_eq!(
|
||||
clock
|
||||
.now_with_future_tolerance(Duration::from_secs(0))
|
||||
.unwrap(),
|
||||
Slot::new(0),
|
||||
"future tolerance of zero should return current slot"
|
||||
);
|
||||
assert_eq!(
|
||||
clock
|
||||
.now_with_past_tolerance(Duration::from_secs(0))
|
||||
.unwrap(),
|
||||
Slot::new(0),
|
||||
"past tolerance of zero should return current slot"
|
||||
);
|
||||
assert_eq!(
|
||||
clock
|
||||
.now_with_future_tolerance(Duration::from_millis(10))
|
||||
.unwrap(),
|
||||
Slot::new(0),
|
||||
"insignificant future tolerance should return current slot"
|
||||
);
|
||||
assert_eq!(
|
||||
clock
|
||||
.now_with_past_tolerance(Duration::from_millis(10))
|
||||
.unwrap(),
|
||||
Slot::new(0),
|
||||
"past tolerance that precedes genesis should return genesis slot"
|
||||
);
|
||||
|
||||
// Set clock to part-way through the 1st slot.
|
||||
*clock.current_time.write() = Duration::from_millis(11_200);
|
||||
assert_eq!(
|
||||
clock
|
||||
.now_with_future_tolerance(Duration::from_secs(0))
|
||||
.unwrap(),
|
||||
Slot::new(1),
|
||||
"future tolerance of zero should return current slot"
|
||||
);
|
||||
assert_eq!(
|
||||
clock
|
||||
.now_with_past_tolerance(Duration::from_secs(0))
|
||||
.unwrap(),
|
||||
Slot::new(1),
|
||||
"past tolerance of zero should return current slot"
|
||||
);
|
||||
assert_eq!(
|
||||
clock
|
||||
.now_with_future_tolerance(Duration::from_millis(800))
|
||||
.unwrap(),
|
||||
Slot::new(2),
|
||||
"significant future tolerance should return next slot"
|
||||
);
|
||||
assert_eq!(
|
||||
clock
|
||||
.now_with_past_tolerance(Duration::from_millis(201))
|
||||
.unwrap(),
|
||||
Slot::new(0),
|
||||
"significant past tolerance should return previous slot"
|
||||
);
|
||||
}
|
||||
}
|
||||
35
common/slot_clock/src/metrics.rs
Normal file
35
common/slot_clock/src/metrics.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use crate::SlotClock;
|
||||
pub use lighthouse_metrics::*;
|
||||
use types::{EthSpec, Slot};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref PRESENT_SLOT: Result<IntGauge> =
|
||||
try_create_int_gauge("slotclock_present_slot", "The present wall-clock slot");
|
||||
pub static ref PRESENT_EPOCH: Result<IntGauge> =
|
||||
try_create_int_gauge("slotclock_present_epoch", "The present wall-clock epoch");
|
||||
pub static ref SLOTS_PER_EPOCH: Result<IntGauge> =
|
||||
try_create_int_gauge("slotclock_slots_per_epoch", "Slots per epoch (constant)");
|
||||
pub static ref MILLISECONDS_PER_SLOT: Result<IntGauge> = try_create_int_gauge(
|
||||
"slotclock_slot_time_milliseconds",
|
||||
"The duration in milliseconds between each slot"
|
||||
);
|
||||
}
|
||||
|
||||
/// Update the global metrics `DEFAULT_REGISTRY` with info from the slot clock.
|
||||
pub fn scrape_for_metrics<T: EthSpec, U: SlotClock>(clock: &U) {
|
||||
let present_slot = match clock.now() {
|
||||
Some(slot) => slot,
|
||||
_ => Slot::new(0),
|
||||
};
|
||||
|
||||
set_gauge(&PRESENT_SLOT, present_slot.as_u64() as i64);
|
||||
set_gauge(
|
||||
&PRESENT_EPOCH,
|
||||
present_slot.epoch(T::slots_per_epoch()).as_u64() as i64,
|
||||
);
|
||||
set_gauge(&SLOTS_PER_EPOCH, T::slots_per_epoch() as i64);
|
||||
set_gauge(
|
||||
&MILLISECONDS_PER_SLOT,
|
||||
clock.slot_duration().as_millis() as i64,
|
||||
);
|
||||
}
|
||||
120
common/slot_clock/src/system_time_slot_clock.rs
Normal file
120
common/slot_clock/src/system_time_slot_clock.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use super::{ManualSlotClock, SlotClock};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use types::Slot;
|
||||
|
||||
pub use std::time::SystemTimeError;
|
||||
|
||||
/// Determines the present slot based upon the present system time.
|
||||
#[derive(Clone)]
|
||||
pub struct SystemTimeSlotClock {
|
||||
clock: ManualSlotClock,
|
||||
}
|
||||
|
||||
impl SlotClock for SystemTimeSlotClock {
|
||||
fn new(genesis_slot: Slot, genesis_duration: Duration, slot_duration: Duration) -> Self {
|
||||
Self {
|
||||
clock: ManualSlotClock::new(genesis_slot, genesis_duration, slot_duration),
|
||||
}
|
||||
}
|
||||
|
||||
fn now(&self) -> Option<Slot> {
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?;
|
||||
self.clock.slot_of(now)
|
||||
}
|
||||
|
||||
fn now_duration(&self) -> Option<Duration> {
|
||||
SystemTime::now().duration_since(UNIX_EPOCH).ok()
|
||||
}
|
||||
|
||||
fn slot_of(&self, now: Duration) -> Option<Slot> {
|
||||
self.clock.slot_of(now)
|
||||
}
|
||||
|
||||
fn duration_to_next_slot(&self) -> Option<Duration> {
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?;
|
||||
self.clock.duration_to_next_slot_from(now)
|
||||
}
|
||||
|
||||
fn duration_to_next_epoch(&self, slots_per_epoch: u64) -> Option<Duration> {
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?;
|
||||
self.clock.duration_to_next_epoch_from(now, slots_per_epoch)
|
||||
}
|
||||
|
||||
fn slot_duration(&self) -> Duration {
|
||||
self.clock.slot_duration()
|
||||
}
|
||||
|
||||
fn duration_to_slot(&self, slot: Slot) -> Option<Duration> {
|
||||
let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?;
|
||||
self.clock.duration_to_slot(slot, now)
|
||||
}
|
||||
|
||||
fn genesis_slot(&self) -> Slot {
|
||||
self.clock.genesis_slot()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/*
|
||||
* Note: these tests are using actual system times and could fail if they are executed on a
|
||||
* very slow machine.
|
||||
*/
|
||||
#[test]
|
||||
fn test_slot_now() {
|
||||
let genesis_slot = Slot::new(0);
|
||||
|
||||
let prior_genesis = |milliseconds_prior: u64| {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("should get system time")
|
||||
- Duration::from_millis(milliseconds_prior)
|
||||
};
|
||||
|
||||
let clock =
|
||||
SystemTimeSlotClock::new(genesis_slot, prior_genesis(0), Duration::from_secs(1));
|
||||
assert_eq!(clock.now(), Some(Slot::new(0)));
|
||||
|
||||
let clock =
|
||||
SystemTimeSlotClock::new(genesis_slot, prior_genesis(5_000), Duration::from_secs(1));
|
||||
assert_eq!(clock.now(), Some(Slot::new(5)));
|
||||
|
||||
let clock =
|
||||
SystemTimeSlotClock::new(genesis_slot, prior_genesis(500), Duration::from_secs(1));
|
||||
assert_eq!(clock.now(), Some(Slot::new(0)));
|
||||
assert!(clock.duration_to_next_slot().unwrap() <= Duration::from_millis(500));
|
||||
|
||||
let clock =
|
||||
SystemTimeSlotClock::new(genesis_slot, prior_genesis(1_500), Duration::from_secs(1));
|
||||
assert_eq!(clock.now(), Some(Slot::new(1)));
|
||||
assert!(clock.duration_to_next_slot().unwrap() <= Duration::from_millis(500));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn zero_seconds() {
|
||||
SystemTimeSlotClock::new(Slot::new(0), Duration::from_secs(0), Duration::from_secs(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn zero_millis() {
|
||||
SystemTimeSlotClock::new(
|
||||
Slot::new(0),
|
||||
Duration::from_secs(0),
|
||||
Duration::from_millis(0),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn less_than_one_millis() {
|
||||
SystemTimeSlotClock::new(
|
||||
Slot::new(0),
|
||||
Duration::from_secs(0),
|
||||
Duration::from_nanos(999),
|
||||
);
|
||||
}
|
||||
}
|
||||
13
common/test_random_derive/Cargo.toml
Normal file
13
common/test_random_derive/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "test_random_derive"
|
||||
version = "0.2.0"
|
||||
authors = ["thojest <thojest@gmail.com>"]
|
||||
edition = "2018"
|
||||
description = "Procedural derive macros for implementation of TestRandom trait"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "1.0.18"
|
||||
quote = "1.0.4"
|
||||
61
common/test_random_derive/src/lib.rs
Normal file
61
common/test_random_derive/src/lib.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
extern crate proc_macro;
|
||||
|
||||
use crate::proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
|
||||
/// Returns true if some field has an attribute declaring it should be generated from default (not
|
||||
/// randomized).
|
||||
///
|
||||
/// The field attribute is: `#[test_random(default)]`
|
||||
fn should_use_default(field: &syn::Field) -> bool {
|
||||
field.attrs.iter().any(|attr| {
|
||||
attr.path.is_ident("test_random") && attr.tokens.to_string().replace(" ", "") == "(default)"
|
||||
})
|
||||
}
|
||||
|
||||
#[proc_macro_derive(TestRandom, attributes(test_random))]
|
||||
pub fn test_random_derive(input: TokenStream) -> TokenStream {
|
||||
let derived_input = parse_macro_input!(input as DeriveInput);
|
||||
let name = &derived_input.ident;
|
||||
let (impl_generics, ty_generics, where_clause) = &derived_input.generics.split_for_impl();
|
||||
|
||||
let struct_data = match &derived_input.data {
|
||||
syn::Data::Struct(s) => s,
|
||||
_ => panic!("test_random_derive only supports structs."),
|
||||
};
|
||||
|
||||
// Build quotes for fields that should be generated and those that should be built from
|
||||
// `Default`.
|
||||
let mut quotes = vec![];
|
||||
for field in &struct_data.fields {
|
||||
match &field.ident {
|
||||
Some(ref ident) => {
|
||||
if should_use_default(field) {
|
||||
quotes.push(quote! {
|
||||
#ident: <_>::default(),
|
||||
});
|
||||
} else {
|
||||
quotes.push(quote! {
|
||||
#ident: <_>::random_for_test(rng),
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => panic!("test_random_derive only supports named struct fields."),
|
||||
};
|
||||
}
|
||||
|
||||
let output = quote! {
|
||||
impl #impl_generics TestRandom for #name #ty_generics #where_clause {
|
||||
fn random_for_test(rng: &mut impl rand::RngCore) -> Self {
|
||||
Self {
|
||||
#(
|
||||
#quotes
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
output.into()
|
||||
}
|
||||
26
common/validator_dir/Cargo.toml
Normal file
26
common/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 = "../../crypto/eth2_wallet" }
|
||||
bls = { path = "../../crypto/bls" }
|
||||
eth2_keystore = { path = "../../crypto/eth2_keystore" }
|
||||
types = { path = "../../consensus/types" }
|
||||
rand = "0.7.2"
|
||||
deposit_contract = { path = "../deposit_contract" }
|
||||
eth2_ssz = { path = "../../consensus/ssz" }
|
||||
eth2_ssz_derive = { path = "../../consensus/ssz_derive" }
|
||||
rayon = "1.3.0"
|
||||
tree_hash = { path = "../../consensus/tree_hash" }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.1.0"
|
||||
286
common/validator_dir/src/builder.rs
Normal file
286
common/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
common/validator_dir/src/insecure_keys.rs
Normal file
67
common/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
common/validator_dir/src/lib.rs
Normal file
21
common/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
common/validator_dir/src/manager.rs
Normal file
115
common/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
common/validator_dir/src/unencrypted_keys.rs
Normal file
66
common/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
common/validator_dir/src/validator_dir.rs
Normal file
230
common/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
common/validator_dir/tests/tests.rs
Normal file
273
common/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