mirror of
https://github.com/sigp/lighthouse.git
synced 2026-03-03 00:31:50 +00:00
Implement VC API (#1657)
## Issue Addressed
NA
## Proposed Changes
- Implements a HTTP API for the validator client.
- Creates EIP-2335 keystores with an empty `description` field, instead of a missing `description` field. Adds option to set name.
- Be more graceful with setups without any validators (yet)
- Remove an error log when there are no validators.
- Create the `validator` dir if it doesn't exist.
- Allow building a `ValidatorDir` without a withdrawal keystore (required for the API method where we only post a voting keystore).
- Add optional `description` field to `validator_definitions.yml`
## TODO
- [x] Signature header, as per https://github.com/sigp/lighthouse/issues/1269#issuecomment-649879855
- [x] Return validator descriptions
- [x] Return deposit data
- [x] Respect the mnemonic offset
- [x] Check that mnemonic can derive returned keys
- [x] Be strict about non-localhost
- [x] Allow graceful start without any validators (+ create validator dir)
- [x] Docs final pass
- [x] Swap to EIP-2335 description field.
- [x] Fix Zerioze TODO in VC api types.
- [x] Zeroize secp256k1 key
## Endpoints
- [x] `GET /lighthouse/version`
- [x] `GET /lighthouse/health`
- [x] `GET /lighthouse/validators`
- [x] `POST /lighthouse/validators/hd`
- [x] `POST /lighthouse/validators/keystore`
- [x] `PATCH /lighthouse/validators/:validator_pubkey`
- [ ] ~~`POST /lighthouse/validators/:validator_pubkey/exit/:epoch`~~ Future works
## Additional Info
TBC
This commit is contained in:
@@ -2,7 +2,10 @@
|
||||
//! Lighthouse project.
|
||||
|
||||
use eth2_keystore::Keystore;
|
||||
use eth2_wallet::Wallet;
|
||||
use eth2_wallet::{
|
||||
bip39::{Language, Mnemonic, MnemonicType},
|
||||
Wallet,
|
||||
};
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::fs::{self, File};
|
||||
@@ -15,6 +18,7 @@ use zeroize::Zeroize;
|
||||
pub mod validator_definitions;
|
||||
|
||||
pub use eth2_keystore;
|
||||
pub use eth2_wallet;
|
||||
pub use eth2_wallet::PlainText;
|
||||
|
||||
/// The minimum number of characters required for a wallet password.
|
||||
@@ -150,6 +154,16 @@ pub fn is_password_sufficiently_complex(password: &[u8]) -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a random 24-word english mnemonic.
|
||||
pub fn random_mnemonic() -> Mnemonic {
|
||||
Mnemonic::new(MnemonicType::Words24, Language::English)
|
||||
}
|
||||
|
||||
/// Attempts to parse a mnemonic phrase.
|
||||
pub fn mnemonic_from_phrase(phrase: &str) -> Result<Mnemonic, String> {
|
||||
Mnemonic::from_phrase(phrase, Language::English).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Provides a new-type wrapper around `String` that is zeroized on `Drop`.
|
||||
///
|
||||
/// Useful for ensuring that password memory is zeroed-out on drop.
|
||||
@@ -164,6 +178,12 @@ impl From<String> for ZeroizeString {
|
||||
}
|
||||
}
|
||||
|
||||
impl ZeroizeString {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for ZeroizeString {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
|
||||
@@ -63,6 +63,8 @@ pub enum SigningDefinition {
|
||||
pub struct ValidatorDefinition {
|
||||
pub enabled: bool,
|
||||
pub voting_public_key: PublicKey,
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
#[serde(flatten)]
|
||||
pub signing_definition: SigningDefinition,
|
||||
}
|
||||
@@ -88,6 +90,7 @@ impl ValidatorDefinition {
|
||||
Ok(ValidatorDefinition {
|
||||
enabled: true,
|
||||
voting_public_key,
|
||||
description: keystore.description().unwrap_or_else(|| "").to_string(),
|
||||
signing_definition: SigningDefinition::LocalKeystore {
|
||||
voting_keystore_path,
|
||||
voting_keystore_password_path: None,
|
||||
@@ -205,6 +208,7 @@ impl ValidatorDefinitions {
|
||||
Some(ValidatorDefinition {
|
||||
enabled: true,
|
||||
voting_public_key,
|
||||
description: keystore.description().unwrap_or_else(|| "").to_string(),
|
||||
signing_definition: SigningDefinition::LocalKeystore {
|
||||
voting_keystore_path,
|
||||
voting_keystore_password_path,
|
||||
|
||||
@@ -15,6 +15,12 @@ reqwest = { version = "0.10.8", features = ["json"] }
|
||||
eth2_libp2p = { path = "../../beacon_node/eth2_libp2p" }
|
||||
proto_array = { path = "../../consensus/proto_array", optional = true }
|
||||
serde_utils = { path = "../../consensus/serde_utils" }
|
||||
zeroize = { version = "1.0.0", features = ["zeroize_derive"] }
|
||||
eth2_keystore = { path = "../../crypto/eth2_keystore" }
|
||||
libsecp256k1 = "0.3.5"
|
||||
ring = "0.16.12"
|
||||
bytes = "0.5.6"
|
||||
account_utils = { path = "../../common/account_utils" }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
psutil = { version = "3.1.0", optional = true }
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#[cfg(feature = "lighthouse")]
|
||||
pub mod lighthouse;
|
||||
pub mod lighthouse_vc;
|
||||
pub mod types;
|
||||
|
||||
use self::types::*;
|
||||
@@ -30,6 +31,14 @@ pub enum Error {
|
||||
StatusCode(StatusCode),
|
||||
/// The supplied URL is badly formatted. It should look something like `http://127.0.0.1:5052`.
|
||||
InvalidUrl(Url),
|
||||
/// The supplied validator client secret is invalid.
|
||||
InvalidSecret(String),
|
||||
/// The server returned a response with an invalid signature. It may be an impostor.
|
||||
InvalidSignatureHeader,
|
||||
/// The server returned a response without a signature header. It may be an impostor.
|
||||
MissingSignatureHeader,
|
||||
/// The server returned an invalid JSON response.
|
||||
InvalidJson(serde_json::Error),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
@@ -40,6 +49,10 @@ impl Error {
|
||||
Error::ServerMessage(msg) => StatusCode::try_from(msg.code).ok(),
|
||||
Error::StatusCode(status) => Some(*status),
|
||||
Error::InvalidUrl(_) => None,
|
||||
Error::InvalidSecret(_) => None,
|
||||
Error::InvalidSignatureHeader => None,
|
||||
Error::MissingSignatureHeader => None,
|
||||
Error::InvalidJson(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -531,7 +544,7 @@ impl BeaconNodeHttpClient {
|
||||
self.get(path).await
|
||||
}
|
||||
|
||||
/// `GET config/fork_schedule`
|
||||
/// `GET config/spec`
|
||||
pub async fn get_config_spec(&self) -> Result<GenericResponse<YamlConfig>, Error> {
|
||||
let mut path = self.eth_path()?;
|
||||
|
||||
|
||||
331
common/eth2/src/lighthouse_vc/http_client.rs
Normal file
331
common/eth2/src/lighthouse_vc/http_client.rs
Normal file
@@ -0,0 +1,331 @@
|
||||
use super::{types::*, PK_LEN, SECRET_PREFIX};
|
||||
use crate::Error;
|
||||
use account_utils::ZeroizeString;
|
||||
use bytes::Bytes;
|
||||
use reqwest::{
|
||||
header::{HeaderMap, HeaderValue},
|
||||
IntoUrl,
|
||||
};
|
||||
use ring::digest::{digest, SHA256};
|
||||
use secp256k1::{Message, PublicKey, Signature};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
|
||||
pub use reqwest;
|
||||
pub use reqwest::{Response, StatusCode, Url};
|
||||
|
||||
/// A wrapper around `reqwest::Client` which provides convenience methods for interfacing with a
|
||||
/// Lighthouse Validator Client HTTP server (`validator_client/src/http_api`).
|
||||
#[derive(Clone)]
|
||||
pub struct ValidatorClientHttpClient {
|
||||
client: reqwest::Client,
|
||||
server: Url,
|
||||
secret: ZeroizeString,
|
||||
server_pubkey: PublicKey,
|
||||
}
|
||||
|
||||
/// Parse an API token and return a secp256k1 public key.
|
||||
pub fn parse_pubkey(secret: &str) -> Result<PublicKey, Error> {
|
||||
let secret = if !secret.starts_with(SECRET_PREFIX) {
|
||||
return Err(Error::InvalidSecret(format!(
|
||||
"secret does not start with {}",
|
||||
SECRET_PREFIX
|
||||
)));
|
||||
} else {
|
||||
&secret[SECRET_PREFIX.len()..]
|
||||
};
|
||||
|
||||
serde_utils::hex::decode(&secret)
|
||||
.map_err(|e| Error::InvalidSecret(format!("invalid hex: {:?}", e)))
|
||||
.and_then(|bytes| {
|
||||
if bytes.len() != PK_LEN {
|
||||
return Err(Error::InvalidSecret(format!(
|
||||
"expected {} bytes not {}",
|
||||
PK_LEN,
|
||||
bytes.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut arr = [0; PK_LEN];
|
||||
arr.copy_from_slice(&bytes);
|
||||
PublicKey::parse_compressed(&arr)
|
||||
.map_err(|e| Error::InvalidSecret(format!("invalid secp256k1 pubkey: {:?}", e)))
|
||||
})
|
||||
}
|
||||
|
||||
impl ValidatorClientHttpClient {
|
||||
pub fn new(server: Url, secret: String) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
client: reqwest::Client::new(),
|
||||
server,
|
||||
server_pubkey: parse_pubkey(&secret)?,
|
||||
secret: secret.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_components(
|
||||
server: Url,
|
||||
client: reqwest::Client,
|
||||
secret: String,
|
||||
) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
client,
|
||||
server,
|
||||
server_pubkey: parse_pubkey(&secret)?,
|
||||
secret: secret.into(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn signed_body(&self, response: Response) -> Result<Bytes, Error> {
|
||||
let sig = response
|
||||
.headers()
|
||||
.get("Signature")
|
||||
.ok_or_else(|| Error::MissingSignatureHeader)?
|
||||
.to_str()
|
||||
.map_err(|_| Error::InvalidSignatureHeader)?
|
||||
.to_string();
|
||||
|
||||
let body = response.bytes().await.map_err(Error::Reqwest)?;
|
||||
|
||||
let message =
|
||||
Message::parse_slice(digest(&SHA256, &body).as_ref()).expect("sha256 is 32 bytes");
|
||||
|
||||
serde_utils::hex::decode(&sig)
|
||||
.ok()
|
||||
.and_then(|bytes| {
|
||||
let sig = Signature::parse_der(&bytes).ok()?;
|
||||
Some(secp256k1::verify(&message, &sig, &self.server_pubkey))
|
||||
})
|
||||
.filter(|is_valid| *is_valid)
|
||||
.ok_or_else(|| Error::InvalidSignatureHeader)?;
|
||||
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
async fn signed_json<T: DeserializeOwned>(&self, response: Response) -> Result<T, Error> {
|
||||
let body = self.signed_body(response).await?;
|
||||
serde_json::from_slice(&body).map_err(Error::InvalidJson)
|
||||
}
|
||||
|
||||
fn headers(&self) -> Result<HeaderMap, Error> {
|
||||
let header_value = HeaderValue::from_str(&format!("Basic {}", self.secret.as_str()))
|
||||
.map_err(|e| {
|
||||
Error::InvalidSecret(format!("secret is invalid as a header value: {}", e))
|
||||
})?;
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Authorization", header_value);
|
||||
|
||||
Ok(headers)
|
||||
}
|
||||
|
||||
/// Perform a HTTP GET request.
|
||||
async fn get<T: DeserializeOwned, U: IntoUrl>(&self, url: U) -> Result<T, Error> {
|
||||
let response = self
|
||||
.client
|
||||
.get(url)
|
||||
.headers(self.headers()?)
|
||||
.send()
|
||||
.await
|
||||
.map_err(Error::Reqwest)?;
|
||||
let response = ok_or_error(response).await?;
|
||||
self.signed_json(response).await
|
||||
}
|
||||
|
||||
/// Perform a HTTP GET request, returning `None` on a 404 error.
|
||||
async fn get_opt<T: DeserializeOwned, U: IntoUrl>(&self, url: U) -> Result<Option<T>, Error> {
|
||||
let response = self
|
||||
.client
|
||||
.get(url)
|
||||
.headers(self.headers()?)
|
||||
.send()
|
||||
.await
|
||||
.map_err(Error::Reqwest)?;
|
||||
match ok_or_error(response).await {
|
||||
Ok(resp) => self.signed_json(resp).await.map(Option::Some),
|
||||
Err(err) => {
|
||||
if err.status() == Some(StatusCode::NOT_FOUND) {
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform a HTTP POST request.
|
||||
async fn post<T: Serialize, U: IntoUrl, V: DeserializeOwned>(
|
||||
&self,
|
||||
url: U,
|
||||
body: &T,
|
||||
) -> Result<V, Error> {
|
||||
let response = self
|
||||
.client
|
||||
.post(url)
|
||||
.headers(self.headers()?)
|
||||
.json(body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(Error::Reqwest)?;
|
||||
let response = ok_or_error(response).await?;
|
||||
self.signed_json(response).await
|
||||
}
|
||||
|
||||
/// Perform a HTTP PATCH request.
|
||||
async fn patch<T: Serialize, U: IntoUrl>(&self, url: U, body: &T) -> Result<(), Error> {
|
||||
let response = self
|
||||
.client
|
||||
.patch(url)
|
||||
.headers(self.headers()?)
|
||||
.json(body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(Error::Reqwest)?;
|
||||
let response = ok_or_error(response).await?;
|
||||
self.signed_body(response).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `GET lighthouse/version`
|
||||
pub async fn get_lighthouse_version(&self) -> Result<GenericResponse<VersionData>, Error> {
|
||||
let mut path = self.server.clone();
|
||||
|
||||
path.path_segments_mut()
|
||||
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
||||
.push("lighthouse")
|
||||
.push("version");
|
||||
|
||||
self.get(path).await
|
||||
}
|
||||
|
||||
/// `GET lighthouse/health`
|
||||
pub async fn get_lighthouse_health(&self) -> Result<GenericResponse<Health>, Error> {
|
||||
let mut path = self.server.clone();
|
||||
|
||||
path.path_segments_mut()
|
||||
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
||||
.push("lighthouse")
|
||||
.push("health");
|
||||
|
||||
self.get(path).await
|
||||
}
|
||||
|
||||
/// `GET lighthouse/spec`
|
||||
pub async fn get_lighthouse_spec(&self) -> Result<GenericResponse<YamlConfig>, Error> {
|
||||
let mut path = self.server.clone();
|
||||
|
||||
path.path_segments_mut()
|
||||
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
||||
.push("lighthouse")
|
||||
.push("spec");
|
||||
|
||||
self.get(path).await
|
||||
}
|
||||
|
||||
/// `GET lighthouse/validators`
|
||||
pub async fn get_lighthouse_validators(
|
||||
&self,
|
||||
) -> Result<GenericResponse<Vec<ValidatorData>>, Error> {
|
||||
let mut path = self.server.clone();
|
||||
|
||||
path.path_segments_mut()
|
||||
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
||||
.push("lighthouse")
|
||||
.push("validators");
|
||||
|
||||
self.get(path).await
|
||||
}
|
||||
|
||||
/// `GET lighthouse/validators/{validator_pubkey}`
|
||||
pub async fn get_lighthouse_validators_pubkey(
|
||||
&self,
|
||||
validator_pubkey: &PublicKeyBytes,
|
||||
) -> Result<Option<GenericResponse<ValidatorData>>, Error> {
|
||||
let mut path = self.server.clone();
|
||||
|
||||
path.path_segments_mut()
|
||||
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
||||
.push("lighthouse")
|
||||
.push("validators")
|
||||
.push(&validator_pubkey.to_string());
|
||||
|
||||
self.get_opt(path).await
|
||||
}
|
||||
|
||||
/// `POST lighthouse/validators`
|
||||
pub async fn post_lighthouse_validators(
|
||||
&self,
|
||||
validators: Vec<ValidatorRequest>,
|
||||
) -> Result<GenericResponse<PostValidatorsResponseData>, Error> {
|
||||
let mut path = self.server.clone();
|
||||
|
||||
path.path_segments_mut()
|
||||
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
||||
.push("lighthouse")
|
||||
.push("validators");
|
||||
|
||||
self.post(path, &validators).await
|
||||
}
|
||||
|
||||
/// `POST lighthouse/validators/mnemonic`
|
||||
pub async fn post_lighthouse_validators_mnemonic(
|
||||
&self,
|
||||
request: &CreateValidatorsMnemonicRequest,
|
||||
) -> Result<GenericResponse<Vec<CreatedValidator>>, Error> {
|
||||
let mut path = self.server.clone();
|
||||
|
||||
path.path_segments_mut()
|
||||
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
||||
.push("lighthouse")
|
||||
.push("validators")
|
||||
.push("mnemonic");
|
||||
|
||||
self.post(path, &request).await
|
||||
}
|
||||
|
||||
/// `POST lighthouse/validators/keystore`
|
||||
pub async fn post_lighthouse_validators_keystore(
|
||||
&self,
|
||||
request: &KeystoreValidatorsPostRequest,
|
||||
) -> Result<GenericResponse<ValidatorData>, Error> {
|
||||
let mut path = self.server.clone();
|
||||
|
||||
path.path_segments_mut()
|
||||
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
||||
.push("lighthouse")
|
||||
.push("validators")
|
||||
.push("keystore");
|
||||
|
||||
self.post(path, &request).await
|
||||
}
|
||||
|
||||
/// `PATCH lighthouse/validators/{validator_pubkey}`
|
||||
pub async fn patch_lighthouse_validators(
|
||||
&self,
|
||||
voting_pubkey: &PublicKeyBytes,
|
||||
enabled: bool,
|
||||
) -> Result<(), Error> {
|
||||
let mut path = self.server.clone();
|
||||
|
||||
path.path_segments_mut()
|
||||
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
||||
.push("lighthouse")
|
||||
.push("validators")
|
||||
.push(&voting_pubkey.to_string());
|
||||
|
||||
self.patch(path, &ValidatorPatchRequest { enabled }).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `Ok(response)` if the response is a `200 OK` response. Otherwise, creates an
|
||||
/// appropriate error message.
|
||||
async fn ok_or_error(response: Response) -> Result<Response, Error> {
|
||||
let status = response.status();
|
||||
|
||||
if status == StatusCode::OK {
|
||||
Ok(response)
|
||||
} else if let Ok(message) = response.json().await {
|
||||
Err(Error::ServerMessage(message))
|
||||
} else {
|
||||
Err(Error::StatusCode(status))
|
||||
}
|
||||
}
|
||||
9
common/eth2/src/lighthouse_vc/mod.rs
Normal file
9
common/eth2/src/lighthouse_vc/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub mod http_client;
|
||||
pub mod types;
|
||||
|
||||
/// The number of bytes in the secp256k1 public key used as the authorization token for the VC API.
|
||||
pub const PK_LEN: usize = 33;
|
||||
|
||||
/// The prefix for the secp256k1 public key when it is used as the authorization token for the VC
|
||||
/// API.
|
||||
pub const SECRET_PREFIX: &str = "api-token-";
|
||||
58
common/eth2/src/lighthouse_vc/types.rs
Normal file
58
common/eth2/src/lighthouse_vc/types.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use account_utils::ZeroizeString;
|
||||
use eth2_keystore::Keystore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use crate::lighthouse::Health;
|
||||
pub use crate::types::{GenericResponse, VersionData};
|
||||
pub use types::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ValidatorData {
|
||||
pub enabled: bool,
|
||||
pub description: String,
|
||||
pub voting_pubkey: PublicKeyBytes,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ValidatorRequest {
|
||||
pub enable: bool,
|
||||
pub description: String,
|
||||
#[serde(with = "serde_utils::quoted_u64")]
|
||||
pub deposit_gwei: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CreateValidatorsMnemonicRequest {
|
||||
pub mnemonic: ZeroizeString,
|
||||
#[serde(with = "serde_utils::quoted_u32")]
|
||||
pub key_derivation_path_offset: u32,
|
||||
pub validators: Vec<ValidatorRequest>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CreatedValidator {
|
||||
pub enabled: bool,
|
||||
pub description: String,
|
||||
pub voting_pubkey: PublicKeyBytes,
|
||||
pub eth1_deposit_tx_data: String,
|
||||
#[serde(with = "serde_utils::quoted_u64")]
|
||||
pub deposit_gwei: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PostValidatorsResponseData {
|
||||
pub mnemonic: ZeroizeString,
|
||||
pub validators: Vec<CreatedValidator>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ValidatorPatchRequest {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct KeystoreValidatorsPostRequest {
|
||||
pub password: ZeroizeString,
|
||||
pub enable: bool,
|
||||
pub keystore: Keystore,
|
||||
}
|
||||
@@ -40,6 +40,7 @@ pub enum Error {
|
||||
UninitializedWithdrawalKeystore,
|
||||
#[cfg(feature = "insecure_keys")]
|
||||
InsecureKeysError(String),
|
||||
MissingPasswordDir,
|
||||
}
|
||||
|
||||
impl From<KeystoreError> for Error {
|
||||
@@ -51,7 +52,7 @@ impl From<KeystoreError> for Error {
|
||||
/// A builder for creating a `ValidatorDir`.
|
||||
pub struct Builder<'a> {
|
||||
base_validators_dir: PathBuf,
|
||||
password_dir: PathBuf,
|
||||
password_dir: Option<PathBuf>,
|
||||
pub(crate) voting_keystore: Option<(Keystore, PlainText)>,
|
||||
pub(crate) withdrawal_keystore: Option<(Keystore, PlainText)>,
|
||||
store_withdrawal_keystore: bool,
|
||||
@@ -60,10 +61,10 @@ pub struct Builder<'a> {
|
||||
|
||||
impl<'a> Builder<'a> {
|
||||
/// Instantiate a new builder.
|
||||
pub fn new(base_validators_dir: PathBuf, password_dir: PathBuf) -> Self {
|
||||
pub fn new(base_validators_dir: PathBuf) -> Self {
|
||||
Self {
|
||||
base_validators_dir,
|
||||
password_dir,
|
||||
password_dir: None,
|
||||
voting_keystore: None,
|
||||
withdrawal_keystore: None,
|
||||
store_withdrawal_keystore: true,
|
||||
@@ -71,6 +72,12 @@ impl<'a> Builder<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Supply a directory in which to store the passwords for the validator keystores.
|
||||
pub fn password_dir<P: Into<PathBuf>>(mut self, password_dir: P) -> Self {
|
||||
self.password_dir = Some(password_dir.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the `ValidatorDir` use the given `keystore` which can be unlocked with `password`.
|
||||
///
|
||||
/// The builder will not necessarily check that `password` can unlock `keystore`.
|
||||
@@ -215,26 +222,35 @@ impl<'a> Builder<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Only the withdrawal keystore if explicitly required.
|
||||
if self.store_withdrawal_keystore {
|
||||
// Write the withdrawal password to file.
|
||||
write_password_to_file(
|
||||
self.password_dir
|
||||
.join(withdrawal_keypair.pk.to_hex_string()),
|
||||
withdrawal_password.as_bytes(),
|
||||
)?;
|
||||
if self.password_dir.is_none() && self.store_withdrawal_keystore {
|
||||
return Err(Error::MissingPasswordDir);
|
||||
}
|
||||
|
||||
// Write the withdrawal keystore to file.
|
||||
write_keystore_to_file(dir.join(WITHDRAWAL_KEYSTORE_FILE), &withdrawal_keystore)?;
|
||||
if let Some(password_dir) = self.password_dir.as_ref() {
|
||||
// Only the withdrawal keystore if explicitly required.
|
||||
if self.store_withdrawal_keystore {
|
||||
// Write the withdrawal password to file.
|
||||
write_password_to_file(
|
||||
password_dir.join(withdrawal_keypair.pk.to_hex_string()),
|
||||
withdrawal_password.as_bytes(),
|
||||
)?;
|
||||
|
||||
// Write the withdrawal keystore to file.
|
||||
write_keystore_to_file(
|
||||
dir.join(WITHDRAWAL_KEYSTORE_FILE),
|
||||
&withdrawal_keystore,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write the voting password to file.
|
||||
write_password_to_file(
|
||||
self.password_dir
|
||||
.join(format!("0x{}", voting_keystore.pubkey())),
|
||||
voting_password.as_bytes(),
|
||||
)?;
|
||||
if let Some(password_dir) = self.password_dir.as_ref() {
|
||||
// Write the voting password to file.
|
||||
write_password_to_file(
|
||||
password_dir.join(format!("0x{}", voting_keystore.pubkey())),
|
||||
voting_password.as_bytes(),
|
||||
)?;
|
||||
}
|
||||
|
||||
// Write the voting keystore to file.
|
||||
write_keystore_to_file(dir.join(VOTING_KEYSTORE_FILE), &voting_keystore)?;
|
||||
|
||||
@@ -73,7 +73,8 @@ pub fn build_deterministic_validator_dirs(
|
||||
indices: &[usize],
|
||||
) -> Result<(), String> {
|
||||
for &i in indices {
|
||||
Builder::new(validators_dir.clone(), password_dir.clone())
|
||||
Builder::new(validators_dir.clone())
|
||||
.password_dir(password_dir.clone())
|
||||
.insecure_voting_keypair(i)
|
||||
.map_err(|e| format!("Unable to generate insecure keypair: {:?}", e))?
|
||||
.store_withdrawal_keystore(false)
|
||||
|
||||
@@ -129,6 +129,11 @@ impl ValidatorDir {
|
||||
&self.dir
|
||||
}
|
||||
|
||||
/// Returns the path to the voting keystore JSON file.
|
||||
pub fn voting_keystore_path(&self) -> PathBuf {
|
||||
self.dir.join(VOTING_KEYSTORE_FILE)
|
||||
}
|
||||
|
||||
/// Attempts to read the keystore in `self.dir` and decrypt the keypair using a password file
|
||||
/// in `password_dir`.
|
||||
///
|
||||
|
||||
@@ -78,13 +78,11 @@ impl Harness {
|
||||
* Build the `ValidatorDir`.
|
||||
*/
|
||||
|
||||
let builder = Builder::new(
|
||||
self.validators_dir.path().into(),
|
||||
self.password_dir.path().into(),
|
||||
)
|
||||
// Note: setting the withdrawal keystore here ensure that it can get overriden by later
|
||||
// calls to `random_withdrawal_keystore`.
|
||||
.store_withdrawal_keystore(config.store_withdrawal_keystore);
|
||||
let builder = Builder::new(self.validators_dir.path().into())
|
||||
.password_dir(self.password_dir.path())
|
||||
// Note: setting the withdrawal keystore here ensure that it can get replaced by
|
||||
// further calls to `random_withdrawal_keystore`.
|
||||
.store_withdrawal_keystore(config.store_withdrawal_keystore);
|
||||
|
||||
let builder = if config.random_voting_keystore {
|
||||
builder.random_voting_keystore().unwrap()
|
||||
@@ -208,13 +206,11 @@ fn without_voting_keystore() {
|
||||
let harness = Harness::new();
|
||||
|
||||
assert!(matches!(
|
||||
Builder::new(
|
||||
harness.validators_dir.path().into(),
|
||||
harness.password_dir.path().into(),
|
||||
)
|
||||
.random_withdrawal_keystore()
|
||||
.unwrap()
|
||||
.build(),
|
||||
Builder::new(harness.validators_dir.path().into(),)
|
||||
.password_dir(harness.password_dir.path())
|
||||
.random_withdrawal_keystore()
|
||||
.unwrap()
|
||||
.build(),
|
||||
Err(BuilderError::UninitializedVotingKeystore)
|
||||
))
|
||||
}
|
||||
@@ -225,26 +221,22 @@ fn without_withdrawal_keystore() {
|
||||
let spec = &MainnetEthSpec::default_spec();
|
||||
|
||||
// Should build without withdrawal keystore if not storing the it or creating eth1 data.
|
||||
Builder::new(
|
||||
harness.validators_dir.path().into(),
|
||||
harness.password_dir.path().into(),
|
||||
)
|
||||
.random_voting_keystore()
|
||||
.unwrap()
|
||||
.store_withdrawal_keystore(false)
|
||||
.build()
|
||||
.unwrap();
|
||||
Builder::new(harness.validators_dir.path().into())
|
||||
.password_dir(harness.password_dir.path())
|
||||
.random_voting_keystore()
|
||||
.unwrap()
|
||||
.store_withdrawal_keystore(false)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
Builder::new(
|
||||
harness.validators_dir.path().into(),
|
||||
harness.password_dir.path().into(),
|
||||
)
|
||||
.random_voting_keystore()
|
||||
.unwrap()
|
||||
.store_withdrawal_keystore(true)
|
||||
.build(),
|
||||
Builder::new(harness.validators_dir.path().into(),)
|
||||
.password_dir(harness.password_dir.path())
|
||||
.random_voting_keystore()
|
||||
.unwrap()
|
||||
.store_withdrawal_keystore(true)
|
||||
.build(),
|
||||
Err(BuilderError::UninitializedWithdrawalKeystore)
|
||||
),
|
||||
"storing the keystore requires keystore"
|
||||
@@ -252,14 +244,12 @@ fn without_withdrawal_keystore() {
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
Builder::new(
|
||||
harness.validators_dir.path().into(),
|
||||
harness.password_dir.path().into(),
|
||||
)
|
||||
.random_voting_keystore()
|
||||
.unwrap()
|
||||
.create_eth1_tx_data(42, spec)
|
||||
.build(),
|
||||
Builder::new(harness.validators_dir.path().into(),)
|
||||
.password_dir(harness.password_dir.path())
|
||||
.random_voting_keystore()
|
||||
.unwrap()
|
||||
.create_eth1_tx_data(42, spec)
|
||||
.build(),
|
||||
Err(BuilderError::UninitializedWithdrawalKeystore)
|
||||
),
|
||||
"storing the keystore requires keystore"
|
||||
|
||||
@@ -13,3 +13,5 @@ types = { path = "../../consensus/types" }
|
||||
beacon_chain = { path = "../../beacon_node/beacon_chain" }
|
||||
state_processing = { path = "../../consensus/state_processing" }
|
||||
safe_arith = { path = "../../consensus/safe_arith" }
|
||||
serde = { version = "1.0.110", features = ["derive"] }
|
||||
tokio = { version = "0.2.21", features = ["sync"] }
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
|
||||
pub mod reject;
|
||||
pub mod reply;
|
||||
pub mod task;
|
||||
|
||||
@@ -101,6 +101,15 @@ pub fn not_synced(msg: String) -> warp::reject::Rejection {
|
||||
warp::reject::custom(NotSynced(msg))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidAuthorization(pub String);
|
||||
|
||||
impl Reject for InvalidAuthorization {}
|
||||
|
||||
pub fn invalid_auth(msg: String) -> warp::reject::Rejection {
|
||||
warp::reject::custom(InvalidAuthorization(msg))
|
||||
}
|
||||
|
||||
/// This function receives a `Rejection` and tries to return a custom
|
||||
/// value, otherwise simply passes the rejection along.
|
||||
pub async fn handle_rejection(err: warp::Rejection) -> Result<impl warp::Reply, Infallible> {
|
||||
@@ -150,6 +159,15 @@ pub async fn handle_rejection(err: warp::Rejection) -> Result<impl warp::Reply,
|
||||
} else if let Some(e) = err.find::<crate::reject::NotSynced>() {
|
||||
code = StatusCode::SERVICE_UNAVAILABLE;
|
||||
message = format!("SERVICE_UNAVAILABLE: beacon node is syncing: {}", e.0);
|
||||
} else if let Some(e) = err.find::<crate::reject::InvalidAuthorization>() {
|
||||
code = StatusCode::FORBIDDEN;
|
||||
message = format!("FORBIDDEN: Invalid auth token: {}", e.0);
|
||||
} else if let Some(e) = err.find::<warp::reject::MissingHeader>() {
|
||||
code = StatusCode::BAD_REQUEST;
|
||||
message = format!("BAD_REQUEST: missing {} header", e.name());
|
||||
} else if let Some(e) = err.find::<warp::reject::InvalidHeader>() {
|
||||
code = StatusCode::BAD_REQUEST;
|
||||
message = format!("BAD_REQUEST: invalid {} header", e.name());
|
||||
} else if err.find::<warp::reject::MethodNotAllowed>().is_some() {
|
||||
code = StatusCode::METHOD_NOT_ALLOWED;
|
||||
message = "METHOD_NOT_ALLOWED".to_string();
|
||||
|
||||
21
common/warp_utils/src/task.rs
Normal file
21
common/warp_utils/src/task.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use serde::Serialize;
|
||||
|
||||
/// Execute some task in a tokio "blocking thread". These threads are ideal for long-running
|
||||
/// (blocking) tasks since they don't jam up the core executor.
|
||||
pub async fn blocking_task<F, T>(func: F) -> T
|
||||
where
|
||||
F: Fn() -> T,
|
||||
{
|
||||
tokio::task::block_in_place(func)
|
||||
}
|
||||
|
||||
/// A convenience wrapper around `blocking_task` for use with `warp` JSON responses.
|
||||
pub async fn blocking_json_task<F, T>(func: F) -> Result<warp::reply::Json, warp::Rejection>
|
||||
where
|
||||
F: Fn() -> Result<T, warp::Rejection>,
|
||||
T: Serialize,
|
||||
{
|
||||
blocking_task(func)
|
||||
.await
|
||||
.map(|resp| warp::reply::json(&resp))
|
||||
}
|
||||
Reference in New Issue
Block a user