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:
Paul Hauner
2020-05-18 21:24:23 +10:00
committed by GitHub
parent c571afb8d8
commit 4331834003
358 changed files with 217 additions and 229 deletions

3
common/README.md Normal file
View File

@@ -0,0 +1,3 @@
# eth2
Common crates containing eth2-specific logic.

View 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"

View 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()
}

View 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]

View 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()
}
}

View 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"

View 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
View File

@@ -0,0 +1 @@
contracts/

View 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"

View 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
}

View 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(&params)
}
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"
);
}
}

View 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" }

View 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");
}
}

View 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"

View 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'}

View 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>>()
}

View 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
)
});
}

View 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
View File

@@ -0,0 +1,2 @@
testnet*
schlesi-*

View 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"

View 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)
}

View 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");
}
}

View 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"

View 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))
}

View 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};

View 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);
}
}
}

View 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");
}
}

View 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"] }

View 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));
}
}

View 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;

View 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"

View 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
View 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
View 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()
}
}

View 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" }

View 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, &param);
});
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)
}
}

View 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"

View 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>,
}

View 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>,
}

View 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};

View 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,
}

View 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,
}

View 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"

View 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()))
}
}

View 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"
);
}
}

View 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,
);
}

View 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),
);
}
}

View 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"

View 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()
}

View 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"

View 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))
}

View 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(())
}

View 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};

View 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)),
))
}
}

View 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,
}
}
}

View 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)
}

View 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);
}