[Remote signer] Fold signer into Lighthouse repository (#1852)

The remote signer relies on the `types` and `crypto/bls` crates from Lighthouse. Moreover, a number of tests of the remote signer consumption of LH leverages this very signer, making any important update a potential dependency nightmare.

Co-authored-by: Paul Hauner <paul@paulhauner.com>
This commit is contained in:
Herman Junge
2020-11-06 06:17:11 +00:00
parent e2ae5010a6
commit e004b98eab
38 changed files with 3211 additions and 6 deletions

View File

@@ -0,0 +1,57 @@
use hyper::{Body, Response, StatusCode};
use serde::{Deserialize, Serialize};
use serde_json::to_string;
use std::error::Error as StdError;
#[derive(PartialEq, Debug, Clone)]
pub enum ApiError {
ServerError(String),
NotImplemented(String),
BadRequest(String),
NotFound(String),
}
#[derive(Deserialize, Serialize)]
pub struct ApiErrorDesc {
pub error: String,
}
pub type ApiResult = Result<Response<Body>, ApiError>;
impl ApiError {
pub fn status_code(self) -> (StatusCode, String) {
match self {
ApiError::ServerError(desc) => (StatusCode::INTERNAL_SERVER_ERROR, desc),
ApiError::NotImplemented(desc) => (StatusCode::NOT_IMPLEMENTED, desc),
ApiError::BadRequest(desc) => (StatusCode::BAD_REQUEST, desc),
ApiError::NotFound(desc) => (StatusCode::NOT_FOUND, desc),
}
}
}
impl Into<Response<Body>> for ApiError {
fn into(self) -> Response<Body> {
let (status_code, desc) = self.status_code();
let json_desc = to_string(&ApiErrorDesc { error: desc })
.expect("The struct ApiErrorDesc should always serialize.");
Response::builder()
.status(status_code)
.body(Body::from(json_desc))
.expect("Response should always be created.")
}
}
impl StdError for ApiError {
fn cause(&self) -> Option<&dyn StdError> {
None
}
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let status = self.clone().status_code();
write!(f, "{:?}: {:?}", status.0, status.1)
}
}

View File

@@ -0,0 +1,18 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
pub struct UpcheckApiResponse {
pub status: String,
}
/// Contains the response to the `get_keys` API.
#[derive(Deserialize, Serialize)]
pub struct KeysApiResponse {
pub keys: Vec<String>,
}
/// Contains the response to the `sign_message` API.
#[derive(Deserialize, Serialize)]
pub struct SignatureApiResponse {
pub signature: String,
}

View File

@@ -0,0 +1,70 @@
use crate::api_error::ApiError;
use crate::api_response::{KeysApiResponse, SignatureApiResponse};
use crate::rest_api::Context;
use crate::signing_root::get_signing_root;
use client_backend::{BackendError, Storage};
use hyper::Request;
use lazy_static::lazy_static;
use regex::Regex;
use std::sync::Arc;
use types::EthSpec;
lazy_static! {
static ref PUBLIC_KEY_FROM_PATH_REGEX: Regex = Regex::new(r"^/[^/]+/([^/]*)").unwrap();
}
/// HTTP handler to get the list of public keys in the backend.
pub fn get_keys<E: EthSpec, S: Storage, U>(
_: U,
ctx: Arc<Context<E, S>>,
) -> Result<KeysApiResponse, ApiError> {
let keys = ctx
.backend
.get_keys()
.map_err(|e| ApiError::ServerError(format!("{}", e)))?;
if keys.is_empty() {
return Err(ApiError::NotFound("No keys found in storage.".to_string()));
}
Ok(KeysApiResponse { keys })
}
/// HTTP handler to sign a message with the requested key.
pub fn sign_message<E: EthSpec, S: Storage>(
req: Request<Vec<u8>>,
ctx: Arc<Context<E, S>>,
) -> Result<SignatureApiResponse, ApiError> {
// Parse the request body and compute the signing root.
let signing_root = get_signing_root::<E>(&req, ctx.spec.clone())?;
// This public key parameter should have been validated by the router.
// We are just going to extract it from the request.
let path = req.uri().path().to_string();
let rc = |path: &str| -> Result<String, String> {
let caps = PUBLIC_KEY_FROM_PATH_REGEX.captures(path).ok_or("")?;
let re_match = caps.get(1).ok_or("")?;
Ok(re_match.as_str().to_string())
};
let public_key = rc(&path).map_err(|_| {
ApiError::BadRequest(format!("Unable to get public key from path: {:?}", path))
})?;
match ctx.backend.sign_message(&public_key, signing_root) {
Ok(signature) => Ok(SignatureApiResponse { signature }),
Err(BackendError::KeyNotFound(_)) => {
Err(ApiError::NotFound(format!("Key not found: {}", public_key)))
}
Err(BackendError::InvalidPublicKey(_)) => Err(ApiError::BadRequest(format!(
"Invalid public key: {}",
public_key
))),
// Catches InvalidSecretKey, KeyMismatch and StorageError.
Err(e) => Err(ApiError::ServerError(e.to_string())),
}
}

View File

@@ -0,0 +1,20 @@
use serde::{Deserialize, Serialize};
use std::net::Ipv4Addr;
/// HTTP REST API Configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
/// The IPv4 address the REST API HTTP server will listen on.
pub listen_address: Ipv4Addr,
/// The port the REST API HTTP server will listen on.
pub port: u16,
}
impl Default for Config {
fn default() -> Self {
Config {
listen_address: Ipv4Addr::new(127, 0, 0, 1),
port: 9000,
}
}
}

View File

@@ -0,0 +1,113 @@
use crate::api_error::{ApiError, ApiResult};
use crate::rest_api::Context;
use hyper::{Body, Request, Response, StatusCode};
use serde::Serialize;
use std::sync::Arc;
use types::EthSpec;
/// Provides a HTTP request handler with specific functionality.
pub struct Handler<E: EthSpec, S: Send + Sync> {
req: Request<()>,
body: Body,
ctx: Arc<Context<E, S>>,
allow_body: bool,
}
impl<E: EthSpec, S: 'static + Send + Sync> Handler<E, S> {
/// Start handling a new request.
pub fn new(req: Request<Body>, ctx: Arc<Context<E, S>>) -> Result<Self, ApiError> {
let (req_parts, body) = req.into_parts();
let req = Request::from_parts(req_parts, ());
Ok(Self {
req,
body,
ctx,
allow_body: false,
})
}
/// Return a simple static value.
///
/// Does not use the blocking executor.
pub async fn static_value<V>(self, value: V) -> Result<HandledRequest<V>, ApiError> {
// Always check and disallow a body for a static value.
let _ = Self::get_body(self.body, false).await?;
Ok(HandledRequest { value })
}
/// The default behaviour is to return an error if any body is supplied in the request. Calling
/// this function disables that error.
pub fn allow_body(mut self) -> Self {
self.allow_body = true;
self
}
/// Spawns `func` on the blocking executor.
///
/// This method is suitable for handling long-running or intensive tasks.
pub async fn in_blocking_task<F, V>(self, func: F) -> Result<HandledRequest<V>, ApiError>
where
V: Send + Sync + 'static,
F: Fn(Request<Vec<u8>>, Arc<Context<E, S>>) -> Result<V, ApiError> + Send + Sync + 'static,
{
let ctx = self.ctx;
let executor = ctx.executor.clone();
let body = Self::get_body(self.body, self.allow_body).await?;
let (req_parts, _) = self.req.into_parts();
let req = Request::from_parts(req_parts, body);
let value = executor
.runtime_handle()
.spawn_blocking(move || func(req, ctx))
.await
.map_err(|e| {
ApiError::ServerError(format!(
"Failed to get blocking join handle: {}",
e.to_string()
))
})??;
Ok(HandledRequest { value })
}
/// Downloads the bytes for `body`.
async fn get_body(body: Body, allow_body: bool) -> Result<Vec<u8>, ApiError> {
let bytes = hyper::body::to_bytes(body)
.await
.map_err(|e| ApiError::ServerError(format!("Unable to get request body: {:?}", e)))?;
if !allow_body && !bytes[..].is_empty() {
Err(ApiError::BadRequest(
"The request body must be empty".to_string(),
))
} else {
Ok(bytes.into_iter().collect())
}
}
}
/// A request that has been "handled" and now a result (`value`) needs to be serialized and
/// returned.
pub struct HandledRequest<V> {
value: V,
}
impl<V: Serialize> HandledRequest<V> {
/// Suitable for items which only implement `serde`.
pub fn serde_encodings(self) -> ApiResult {
let body = Body::from(serde_json::to_string(&self.value).map_err(|e| {
ApiError::ServerError(format!(
"Unable to serialize response body as JSON: {:?}",
e
))
})?);
Response::builder()
.status(StatusCode::OK)
.header("content-type", "application/json")
.body(body)
.map_err(|e| ApiError::ServerError(format!("Failed to build response: {:?}", e)))
}
}

View File

@@ -0,0 +1,58 @@
pub mod api_error;
pub mod api_response;
mod backend;
mod config;
mod handler;
mod rest_api;
mod router;
mod signing_root;
mod upcheck;
use clap::ArgMatches;
use client_backend::Backend;
use config::Config;
use environment::RuntimeContext;
use std::net::Ipv4Addr;
use std::net::SocketAddr;
use types::EthSpec;
pub struct Client {
listening_address: SocketAddr,
}
impl Client {
pub async fn new<E: EthSpec>(
context: RuntimeContext<E>,
cli_args: &ArgMatches<'_>,
) -> Result<Self, String> {
let log = context.executor.log();
let mut config = Config::default();
if let Some(address) = cli_args.value_of("listen-address") {
config.listen_address = address
.parse::<Ipv4Addr>()
.map_err(|_| "listen-address is not a valid IPv4 address.")?;
}
if let Some(port) = cli_args.value_of("port") {
config.port = port
.parse::<u16>()
.map_err(|_| "port is not a valid u16.")?;
}
let backend = Backend::new(cli_args, log)?;
// It is useful to get the listening address if you have set up your port to be 0.
let listening_address =
rest_api::start_server(context.executor, config, backend, context.eth_spec_instance)
.map_err(|e| format!("Failed to start HTTP API: {:?}", e))?;
Ok(Self { listening_address })
}
pub fn get_listening_address(&self) -> SocketAddr {
self.listening_address
}
}

View File

@@ -0,0 +1,91 @@
use crate::config::Config;
use client_backend::{Backend, Storage};
use futures::future::TryFutureExt;
use hyper::server::conn::AddrStream;
use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Request, Server};
use slog::{info, warn};
use std::net::SocketAddr;
use std::sync::Arc;
use task_executor::TaskExecutor;
use types::{ChainSpec, EthSpec};
pub struct Context<E: EthSpec, S: Send + Sync> {
pub config: Config,
pub executor: TaskExecutor,
pub log: slog::Logger,
pub backend: Backend<S>,
pub eth_spec_instance: E,
pub spec: ChainSpec,
}
pub fn start_server<E: EthSpec, S: Storage>(
executor: TaskExecutor,
config: Config,
backend: Backend<S>,
eth_spec_instance: E,
) -> Result<SocketAddr, hyper::Error> {
let log = executor.log();
let context = Arc::new(Context {
executor: executor.clone(),
log: log.clone(),
config: config.clone(),
backend,
eth_spec_instance,
spec: E::default_spec(),
});
// Define the function that will build the request handler.
let make_service = make_service_fn(move |_socket: &AddrStream| {
let ctx = context.clone();
async move {
Ok::<_, hyper::Error>(service_fn(move |req: Request<Body>| {
crate::router::on_http_request(req, ctx.clone())
}))
}
});
let bind_addr = (config.listen_address, config.port).into();
let server = Server::bind(&bind_addr).serve(make_service);
// Determine the address the server is actually listening on.
//
// This may be different to `bind_addr` if bind port was 0 (this allows the OS to choose a free
// port).
let actual_listen_addr = server.local_addr();
// Build a channel to kill the HTTP server.
let exit = executor.exit();
let inner_log = log.clone();
let server_exit = async move {
let _ = exit.await;
info!(inner_log, "HTTP service shutdown");
};
// Configure the `hyper` server to gracefully shutdown when the shutdown channel is triggered.
let inner_log = log.clone();
let server_future = server
.with_graceful_shutdown(async {
server_exit.await;
})
.map_err(move |e| {
warn!(
inner_log,
"HTTP server failed to start, Unable to bind"; "address" => format!("{:?}", e)
)
})
.unwrap_or_else(|_| ());
info!(
log,
"HTTP API started";
"address" => format!("{}", actual_listen_addr.ip()),
"port" => actual_listen_addr.port(),
);
executor.spawn_without_exit(server_future, "http");
Ok(actual_listen_addr)
}

View File

@@ -0,0 +1,101 @@
use crate::api_error::ApiError;
use crate::backend::{get_keys, sign_message};
use crate::handler::Handler;
use crate::rest_api::Context;
use crate::upcheck::upcheck;
use client_backend::Storage;
use hyper::{Body, Method, Request, Response};
use slog::debug;
use std::sync::Arc;
use std::time::Instant;
use types::EthSpec;
pub async fn on_http_request<E: EthSpec, S: Storage>(
req: Request<Body>,
ctx: Arc<Context<E, S>>,
) -> Result<Response<Body>, ApiError> {
let path = req.uri().path().to_string();
let received_instant = Instant::now();
let log = ctx.log.clone();
match route(req, ctx).await {
Ok(response) => {
debug!(
log,
"HTTP API request successful";
"path" => path,
"duration_ms" => Instant::now().duration_since(received_instant).as_millis()
);
Ok(response)
}
Err(error) => {
debug!(
log,
"HTTP API request failure";
"path" => path,
"duration_ms" => Instant::now().duration_since(received_instant).as_millis()
);
Ok(error.into())
}
}
}
async fn route<E: EthSpec, S: Storage>(
req: Request<Body>,
ctx: Arc<Context<E, S>>,
) -> Result<Response<Body>, ApiError> {
let path = req.uri().path().to_string();
let method = req.method().clone();
let ctx = ctx.clone();
let handler = Handler::new(req, ctx)?;
match (method, path.as_ref()) {
(Method::GET, "/upcheck") => handler.static_value(upcheck()).await?.serde_encodings(),
(Method::GET, "/keys") => handler.in_blocking_task(get_keys).await?.serde_encodings(),
(Method::POST, _) => route_post(&path, handler).await,
_ => Err(ApiError::NotFound(
"Request path and/or method not found.".to_string(),
)),
}
}
/// Responds to all the POST requests.
///
/// Should be deprecated once a better routing library is used, such as `warp`
async fn route_post<E: EthSpec, S: Storage>(
path: &str,
handler: Handler<E, S>,
) -> Result<Response<Body>, ApiError> {
let mut path_segments = path[1..].trim_end_matches('/').split('/');
match path_segments.next() {
Some("sign") => {
let path_segments_count = path_segments.clone().count();
if path_segments_count == 0 {
return Err(ApiError::BadRequest(
"Parameter public_key needed in route /sign/:public_key".to_string(),
));
}
if path_segments_count > 1 {
return Err(ApiError::BadRequest(
"Only one path segment is allowed after /sign".to_string(),
));
}
handler
.allow_body()
.in_blocking_task(sign_message)
.await?
.serde_encodings()
}
_ => Err(ApiError::NotFound(
"Request path and/or method not found.".to_string(),
)),
}
}

View File

@@ -0,0 +1,78 @@
use crate::api_error::ApiError;
use serde::Deserialize;
use serde_json::{from_value, Value};
use types::{
AttestationData, BeaconBlock, ChainSpec, Domain, Epoch, EthSpec, Fork, Hash256, SignedRoot,
};
#[derive(Deserialize)]
pub struct SignMessageRequestBody {
/// BLS Signature domain.
/// Supporting `beacon_proposer`, `beacon_attester`, and `randao`.
/// As defined in
/// * https://github.com/ethereum/eth2.0-specs/blob/dev/specs/phase0/beacon-chain.md#domain-types
/// * in lowercase, omitting the `domain` prefix.
bls_domain: String,
/// Supporting `block`, `attestation`, and `epoch`.
/// (In LH these are `BeaconBlock`, `AttestationData`, and `Epoch`).
/// As defined in
/// * https://github.com/ethereum/eth2.0-APIs/blob/master/types/block.yaml
/// * https://github.com/ethereum/eth2.0-APIs/blob/master/types/attestation.yaml
/// * https://github.com/ethereum/eth2.0-APIs/blob/master/types/misc.yaml
data: Value,
/// A `Fork` object containing previous and current versions.
/// As defined in
/// * https://github.com/ethereum/eth2.0-APIs/blob/master/types/misc.yaml
fork: Fork,
/// A `Hash256` for domain separation and chain versioning.
genesis_validators_root: Hash256,
}
pub fn get_signing_root<E: EthSpec>(
req: &hyper::Request<std::vec::Vec<u8>>,
spec: ChainSpec,
) -> Result<Hash256, ApiError> {
let body: SignMessageRequestBody = serde_json::from_slice(req.body()).map_err(|e| {
ApiError::BadRequest(format!("Unable to parse body message from JSON: {:?}", e))
})?;
let get_domain = |epoch, bls_domain| {
spec.get_domain(epoch, bls_domain, &body.fork, body.genesis_validators_root)
};
match body.bls_domain.as_str() {
"beacon_proposer" => {
let block = from_value::<BeaconBlock<E>>(body.data.clone()).map_err(|e| {
ApiError::BadRequest(format!("Unable to parse block from JSON: {:?}", e))
})?;
Ok(block.signing_root(get_domain(block.epoch(), Domain::BeaconProposer)))
}
"beacon_attester" => {
let attestation = from_value::<AttestationData>(body.data.clone()).map_err(|e| {
ApiError::BadRequest(format!("Unable to parse attestation from JSON: {:?}", e))
})?;
Ok(attestation
.signing_root(get_domain(attestation.target.epoch, Domain::BeaconAttester)))
}
"randao" => {
let epoch = from_value::<Epoch>(body.data.clone()).map_err(|e| {
ApiError::BadRequest(format!("Unable to parse attestation from JSON: {:?}", e))
})?;
Ok(epoch.signing_root(get_domain(epoch, Domain::Randao)))
}
s => Err(ApiError::BadRequest(format!(
"Unsupported bls_domain parameter: {}",
s
))),
}
}

View File

@@ -0,0 +1,7 @@
use crate::api_response::UpcheckApiResponse;
pub fn upcheck() -> UpcheckApiResponse {
UpcheckApiResponse {
status: "OK".to_string(),
}
}