diff --git a/beacon_node/client/src/lib.rs b/beacon_node/client/src/lib.rs index 73b0e5aed..d7df3e0d6 100644 --- a/beacon_node/client/src/lib.rs +++ b/beacon_node/client/src/lib.rs @@ -240,6 +240,7 @@ where executor, beacon_chain.clone(), network.clone(), + network_send.clone(), client_config.db_path().expect("unable to read datadir"), eth2_config.clone(), &log, diff --git a/beacon_node/rest_api/Cargo.toml b/beacon_node/rest_api/Cargo.toml index 863ea04da..7ea21eeba 100644 --- a/beacon_node/rest_api/Cargo.toml +++ b/beacon_node/rest_api/Cargo.toml @@ -25,8 +25,7 @@ types = { path = "../../eth2/types" } clap = "2.32.0" http = "^0.1.17" prometheus = { version = "^0.6", features = ["process"] } -hyper = "0.12.32" -futures = "0.1" +hyper = "0.12.34" exit-future = "0.1.3" tokio = "0.1.17" url = "2.0" @@ -35,3 +34,6 @@ eth2_config = { path = "../../eth2/utils/eth2_config" } lighthouse_metrics = { path = "../../eth2/utils/lighthouse_metrics" } slot_clock = { path = "../../eth2/utils/slot_clock" } hex = "0.3.2" +parking_lot = "0.9" +futures = "0.1.25" + diff --git a/beacon_node/rest_api/src/beacon.rs b/beacon_node/rest_api/src/beacon.rs index a4660836d..c1a9da6ee 100644 --- a/beacon_node/rest_api/src/beacon.rs +++ b/beacon_node/rest_api/src/beacon.rs @@ -1,18 +1,25 @@ -use super::{success_response, ApiResult, ResponseBuilder}; -use crate::{helpers::*, ApiError, UrlQuery}; +use crate::helpers::*; +use crate::response_builder::ResponseBuilder; +use crate::{ApiError, ApiResult, UrlQuery}; use beacon_chain::{BeaconChain, BeaconChainTypes}; use hyper::{Body, Request}; use serde::Serialize; use ssz_derive::Encode; use std::sync::Arc; use store::Store; -use types::{BeaconBlock, BeaconState, EthSpec, Hash256, Slot}; +use types::{BeaconBlock, BeaconState, Epoch, EthSpec, Hash256, Slot, Validator}; -#[derive(Serialize)] +#[derive(Serialize, Encode)] pub struct HeadResponse { 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, } /// HTTP handler to return a `BeaconBlock` at a given `root` or `slot`. @@ -22,16 +29,33 @@ pub fn get_head(req: Request) -> ApiResult .get::>>() .ok_or_else(|| ApiError::ServerError("Beacon chain extension missing".to_string()))?; + let chain_head = beacon_chain.head(); + let head = HeadResponse { - slot: beacon_chain.head().beacon_state.slot, - block_root: beacon_chain.head().beacon_block_root, - state_root: beacon_chain.head().beacon_state_root, + slot: chain_head.beacon_state.slot, + block_root: chain_head.beacon_block_root, + state_root: chain_head.beacon_state_root, + finalized_slot: chain_head + .beacon_state + .finalized_checkpoint + .epoch + .start_slot(T::EthSpec::slots_per_epoch()), + finalized_block_root: chain_head.beacon_state.finalized_checkpoint.root, + justified_slot: chain_head + .beacon_state + .current_justified_checkpoint + .epoch + .start_slot(T::EthSpec::slots_per_epoch()), + justified_block_root: chain_head.beacon_state.current_justified_checkpoint.root, + previous_justified_slot: chain_head + .beacon_state + .previous_justified_checkpoint + .epoch + .start_slot(T::EthSpec::slots_per_epoch()), + previous_justified_block_root: chain_head.beacon_state.previous_justified_checkpoint.root, }; - let json: String = serde_json::to_string(&head) - .map_err(|e| ApiError::ServerError(format!("Unable to serialize HeadResponse: {:?}", e)))?; - - Ok(success_response(Body::from(json))) + ResponseBuilder::new(&req)?.body(&head) } #[derive(Serialize, Encode)] @@ -56,7 +80,7 @@ pub fn get_block(req: Request) -> ApiResult let target = parse_slot(&value)?; block_root_at_slot(&beacon_chain, target).ok_or_else(|| { - ApiError::NotFound(format!("Unable to find BeaconBlock for slot {}", target)) + ApiError::NotFound(format!("Unable to find BeaconBlock for slot {:?}", target)) })? } ("root", value) => parse_root(&value)?, @@ -68,7 +92,7 @@ pub fn get_block(req: Request) -> ApiResult .get::>(&block_root)? .ok_or_else(|| { ApiError::NotFound(format!( - "Unable to find BeaconBlock for root {}", + "Unable to find BeaconBlock for root {:?}", block_root )) })?; @@ -78,42 +102,59 @@ pub fn get_block(req: Request) -> ApiResult beacon_block: block, }; - ResponseBuilder::new(&req).body(&response) + ResponseBuilder::new(&req)?.body(&response) } /// HTTP handler to return a `BeaconBlock` root at a given `slot`. pub fn get_block_root(req: Request) -> ApiResult { - let beacon_chain = req - .extensions() - .get::>>() - .ok_or_else(|| ApiError::ServerError("Beacon chain extension missing".to_string()))?; + let beacon_chain = get_beacon_chain_from_request::(&req)?; let slot_string = UrlQuery::from_request(&req)?.only_one("slot")?; let target = parse_slot(&slot_string)?; let root = block_root_at_slot(&beacon_chain, target).ok_or_else(|| { - ApiError::NotFound(format!("Unable to find BeaconBlock for slot {}", target)) + ApiError::NotFound(format!("Unable to find BeaconBlock for slot {:?}", target)) })?; - let json: String = serde_json::to_string(&root) - .map_err(|e| ApiError::ServerError(format!("Unable to serialize root: {:?}", e)))?; - - Ok(success_response(Body::from(json))) + ResponseBuilder::new(&req)?.body(&root) } -/// HTTP handler to return a `BeaconState` at a given `root` or `slot`. +/// HTTP handler to return the `Fork` of the current head. +pub fn get_fork(req: Request) -> ApiResult { + let beacon_chain = get_beacon_chain_from_request::(&req)?; + ResponseBuilder::new(&req)?.body(&beacon_chain.head().beacon_state.fork) +} + +/// HTTP handler to return the set of validators for an `Epoch` /// -/// Will not return a state if the request slot is in the future. Will return states higher than -/// the current head by skipping slots. -pub fn get_genesis_state(req: Request) -> ApiResult { - let beacon_chain = req - .extensions() - .get::>>() - .ok_or_else(|| ApiError::ServerError("Beacon chain extension missing".to_string()))?; +/// The `Epoch` parameter can be any epoch number. If it is not specified, +/// the current epoch is assumed. +pub fn get_validators(req: Request) -> ApiResult { + let beacon_chain = get_beacon_chain_from_request::(&req)?; - let (_root, state) = state_at_slot(&beacon_chain, Slot::new(0))?; + let epoch = match UrlQuery::from_request(&req) { + // We have some parameters, so make sure it's the epoch one and parse it + Ok(query) => query + .only_one("epoch")? + .parse::() + .map(Epoch::from) + .map_err(|e| { + ApiError::BadRequest(format!("Invalid epoch parameter, must be a u64. {:?}", e)) + })?, + // In this case, our url query did not contain any parameters, so we take the default + Err(_) => beacon_chain.epoch().map_err(|e| { + ApiError::ServerError(format!("Unable to determine current epoch: {:?}", e)) + })?, + }; - ResponseBuilder::new(&req).body(&state) + let all_validators = &beacon_chain.head().beacon_state.validators; + let active_vals: Vec = all_validators + .iter() + .filter(|v| v.is_active_at(epoch)) + .cloned() + .collect(); + + ResponseBuilder::new(&req)?.body(&active_vals) } #[derive(Serialize, Encode)] @@ -128,13 +169,23 @@ pub struct StateResponse { /// Will not return a state if the request slot is in the future. Will return states higher than /// the current head by skipping slots. pub fn get_state(req: Request) -> ApiResult { - let beacon_chain = req - .extensions() - .get::>>() - .ok_or_else(|| ApiError::ServerError("Beacon chain extension missing".to_string()))?; + let beacon_chain = get_beacon_chain_from_request::(&req)?; + let head_state = beacon_chain.head().beacon_state; - let query_params = ["root", "slot"]; - let (key, value) = UrlQuery::from_request(&req)?.first_of(&query_params)?; + let (key, value) = match UrlQuery::from_request(&req) { + Ok(query) => { + // We have *some* parameters, just check them. + let query_params = ["root", "slot"]; + query.first_of(&query_params)? + } + Err(ApiError::BadRequest(_)) => { + // No parameters provided at all, use current slot. + (String::from("slot"), head_state.slot.to_string()) + } + Err(e) => { + return Err(e); + } + }; let (root, state): (Hash256, BeaconState) = match (key.as_ref(), value) { ("slot", value) => state_at_slot(&beacon_chain, parse_slot(&value)?)?, @@ -144,7 +195,7 @@ pub fn get_state(req: Request) -> ApiResult let state = beacon_chain .store .get(root)? - .ok_or_else(|| ApiError::NotFound(format!("No state for root: {}", root)))?; + .ok_or_else(|| ApiError::NotFound(format!("No state for root: {:?}", root)))?; (*root, state) } @@ -156,7 +207,7 @@ pub fn get_state(req: Request) -> ApiResult beacon_state: state, }; - ResponseBuilder::new(&req).body(&response) + ResponseBuilder::new(&req)?.body(&response) } /// HTTP handler to return a `BeaconState` root at a given `slot`. @@ -164,39 +215,33 @@ pub fn get_state(req: Request) -> ApiResult /// Will not return a state if the request slot is in the future. Will return states higher than /// the current head by skipping slots. pub fn get_state_root(req: Request) -> ApiResult { - let beacon_chain = req - .extensions() - .get::>>() - .ok_or_else(|| ApiError::ServerError("Beacon chain extension missing".to_string()))?; + let beacon_chain = get_beacon_chain_from_request::(&req)?; let slot_string = UrlQuery::from_request(&req)?.only_one("slot")?; let slot = parse_slot(&slot_string)?; let root = state_root_at_slot(&beacon_chain, slot)?; - let json: String = serde_json::to_string(&root) - .map_err(|e| ApiError::ServerError(format!("Unable to serialize root: {:?}", e)))?; - - Ok(success_response(Body::from(json))) + ResponseBuilder::new(&req)?.body(&root) } /// HTTP handler to return the highest finalized slot. -pub fn get_latest_finalized_checkpoint( +pub fn get_current_finalized_checkpoint( req: Request, ) -> ApiResult { - let beacon_chain = req - .extensions() - .get::>>() - .ok_or_else(|| ApiError::ServerError("Beacon chain extension missing".to_string()))?; + let beacon_chain = get_beacon_chain_from_request::(&req)?; + let head_state = beacon_chain.head().beacon_state; - let checkpoint = beacon_chain - .head() - .beacon_state - .finalized_checkpoint - .clone(); + let checkpoint = head_state.finalized_checkpoint.clone(); - let json: String = serde_json::to_string(&checkpoint) - .map_err(|e| ApiError::ServerError(format!("Unable to serialize checkpoint: {:?}", e)))?; - - Ok(success_response(Body::from(json))) + ResponseBuilder::new(&req)?.body(&checkpoint) +} + +/// HTTP handler to return a `BeaconState` at the genesis block. +pub fn get_genesis_state(req: Request) -> ApiResult { + let beacon_chain = get_beacon_chain_from_request::(&req)?; + + let (_root, state) = state_at_slot(&beacon_chain, Slot::new(0))?; + + ResponseBuilder::new(&req)?.body(&state) } diff --git a/beacon_node/rest_api/src/error.rs b/beacon_node/rest_api/src/error.rs new file mode 100644 index 000000000..9f815a7d3 --- /dev/null +++ b/beacon_node/rest_api/src/error.rs @@ -0,0 +1,86 @@ +use crate::BoxFut; +use hyper::{Body, Response, StatusCode}; +use std::error::Error as StdError; + +#[derive(PartialEq, Debug, Clone)] +pub enum ApiError { + MethodNotAllowed(String), + ServerError(String), + NotImplemented(String), + BadRequest(String), + NotFound(String), + UnsupportedType(String), + ImATeapot(String), // Just in case. + ProcessingError(String), // A 202 error, for when a block/attestation cannot be processed, but still transmitted. +} + +pub type ApiResult = Result, ApiError>; + +impl ApiError { + pub fn status_code(self) -> (StatusCode, String) { + match self { + ApiError::MethodNotAllowed(desc) => (StatusCode::METHOD_NOT_ALLOWED, desc), + 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), + ApiError::UnsupportedType(desc) => (StatusCode::UNSUPPORTED_MEDIA_TYPE, desc), + ApiError::ImATeapot(desc) => (StatusCode::IM_A_TEAPOT, desc), + ApiError::ProcessingError(desc) => (StatusCode::ACCEPTED, desc), + } + } +} + +impl Into> for ApiError { + fn into(self) -> Response { + let status_code = self.status_code(); + Response::builder() + .status(status_code.0) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from(status_code.1)) + .expect("Response should always be created.") + } +} + +impl Into for ApiError { + fn into(self) -> BoxFut { + Box::new(futures::future::err(self)) + } +} + +impl From for ApiError { + fn from(e: store::Error) -> ApiError { + ApiError::ServerError(format!("Database error: {:?}", e)) + } +} + +impl From for ApiError { + fn from(e: types::BeaconStateError) -> ApiError { + ApiError::ServerError(format!("BeaconState error: {:?}", e)) + } +} + +impl From for ApiError { + fn from(e: state_processing::per_slot_processing::Error) -> ApiError { + ApiError::ServerError(format!("PerSlotProcessing error: {:?}", e)) + } +} + +impl From for ApiError { + fn from(e: hyper::error::Error) -> ApiError { + ApiError::ServerError(format!("Networking error: {:?}", e)) + } +} + +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) + } +} diff --git a/beacon_node/rest_api/src/helpers.rs b/beacon_node/rest_api/src/helpers.rs index a21f1831e..a711246b0 100644 --- a/beacon_node/rest_api/src/helpers.rs +++ b/beacon_node/rest_api/src/helpers.rs @@ -1,10 +1,20 @@ use crate::{ApiError, ApiResult}; use beacon_chain::{BeaconChain, BeaconChainTypes}; use bls::PublicKey; +use eth2_libp2p::{PubsubMessage, Topic}; +use eth2_libp2p::{ + BEACON_ATTESTATION_TOPIC, BEACON_BLOCK_TOPIC, TOPIC_ENCODING_POSTFIX, TOPIC_PREFIX, +}; use hex; +use http::header; use hyper::{Body, Request}; +use network::NetworkMessage; +use parking_lot::RwLock; +use ssz::Encode; +use std::sync::Arc; use store::{iter::AncestorIter, Store}; -use types::{BeaconState, EthSpec, Hash256, RelativeEpoch, Slot}; +use tokio::sync::mpsc; +use types::{Attestation, BeaconBlock, BeaconState, EthSpec, Hash256, RelativeEpoch, Slot}; /// Parse a slot from a `0x` preixed string. /// @@ -13,7 +23,22 @@ pub fn parse_slot(string: &str) -> Result { string .parse::() .map(Slot::from) - .map_err(|e| ApiError::InvalidQueryParams(format!("Unable to parse slot: {:?}", e))) + .map_err(|e| ApiError::BadRequest(format!("Unable to parse slot: {:?}", e))) +} + +/// Checks the provided request to ensure that the `content-type` header. +/// +/// The content-type header should either be omitted, in which case JSON is assumed, or it should +/// explicity specify `application/json`. If anything else is provided, an error is returned. +pub fn check_content_type_for_json(req: &Request) -> Result<(), ApiError> { + match req.headers().get(header::CONTENT_TYPE) { + Some(h) if h == "application/json" => Ok(()), + Some(h) => Err(ApiError::BadRequest(format!( + "The provided content-type {:?} is not available, this endpoint only supports json.", + h + ))), + _ => Ok(()), + } } /// Parse a root from a `0x` preixed string. @@ -26,9 +51,9 @@ pub fn parse_root(string: &str) -> Result { let trimmed = string.trim_start_matches(PREFIX); trimmed .parse() - .map_err(|e| ApiError::InvalidQueryParams(format!("Unable to parse root: {:?}", e))) + .map_err(|e| ApiError::BadRequest(format!("Unable to parse root: {:?}", e))) } else { - Err(ApiError::InvalidQueryParams( + Err(ApiError::BadRequest( "Root must have a '0x' prefix".to_string(), )) } @@ -39,13 +64,13 @@ pub fn parse_pubkey(string: &str) -> Result { const PREFIX: &str = "0x"; if string.starts_with(PREFIX) { let pubkey_bytes = hex::decode(string.trim_start_matches(PREFIX)) - .map_err(|e| ApiError::InvalidQueryParams(format!("Invalid hex string: {:?}", e)))?; + .map_err(|e| ApiError::BadRequest(format!("Invalid hex string: {:?}", e)))?; let pubkey = PublicKey::from_bytes(pubkey_bytes.as_slice()).map_err(|e| { - ApiError::InvalidQueryParams(format!("Unable to deserialize public key: {:?}.", e)) + ApiError::BadRequest(format!("Unable to deserialize public key: {:?}.", e)) })?; return Ok(pubkey); } else { - return Err(ApiError::InvalidQueryParams( + return Err(ApiError::BadRequest( "Public key must have a '0x' prefix".to_string(), )); } @@ -122,7 +147,7 @@ pub fn state_root_at_slot( // // We could actually speculate about future state roots by skipping slots, however that's // likely to cause confusion for API users. - Err(ApiError::InvalidQueryParams(format!( + Err(ApiError::BadRequest(format!( "Requested slot {} is past the current slot {}", slot, current_slot ))) @@ -169,6 +194,78 @@ pub fn implementation_pending_response(_req: Request) -> ApiResult { )) } +pub fn get_beacon_chain_from_request( + req: &Request, +) -> Result<(Arc>), ApiError> { + // Get beacon state + let beacon_chain = req + .extensions() + .get::>>() + .ok_or_else(|| ApiError::ServerError("Beacon chain extension missing".into()))?; + + Ok(beacon_chain.clone()) +} + +pub fn get_logger_from_request(req: &Request) -> slog::Logger { + let log = req + .extensions() + .get::() + .expect("Should always get the logger from the request, since we put it in there."); + log.to_owned() +} + +pub fn publish_beacon_block_to_network( + chan: Arc>>, + block: BeaconBlock, +) -> Result<(), ApiError> { + // create the network topic to send on + let topic_string = format!( + "/{}/{}/{}", + TOPIC_PREFIX, BEACON_BLOCK_TOPIC, TOPIC_ENCODING_POSTFIX + ); + let topic = Topic::new(topic_string); + let message = PubsubMessage::Block(block.as_ssz_bytes()); + + // Publish the block to the p2p network via gossipsub. + if let Err(e) = chan.write().try_send(NetworkMessage::Publish { + topics: vec![topic], + message, + }) { + return Err(ApiError::ServerError(format!( + "Unable to send new block to network: {:?}", + e + ))); + } + + Ok(()) +} + +pub fn publish_attestation_to_network( + chan: Arc>>, + attestation: Attestation, +) -> Result<(), ApiError> { + // create the network topic to send on + let topic_string = format!( + "/{}/{}/{}", + TOPIC_PREFIX, BEACON_ATTESTATION_TOPIC, TOPIC_ENCODING_POSTFIX + ); + let topic = Topic::new(topic_string); + let message = PubsubMessage::Attestation(attestation.as_ssz_bytes()); + + // Publish the attestation to the p2p network via gossipsub. + if let Err(e) = chan.write().try_send(NetworkMessage::Publish { + topics: vec![topic], + message, + }) { + return Err(ApiError::ServerError(format!( + "Unable to send new attestation to network: {:?}", + e + ))); + } + + Ok(()) +} + #[cfg(test)] mod test { use super::*; diff --git a/beacon_node/rest_api/src/lib.rs b/beacon_node/rest_api/src/lib.rs index 4aab91e69..133fc3a26 100644 --- a/beacon_node/rest_api/src/lib.rs +++ b/beacon_node/rest_api/src/lib.rs @@ -1,9 +1,12 @@ #[macro_use] +mod macros; +#[macro_use] extern crate lazy_static; extern crate network as client_network; mod beacon; mod config; +mod error; mod helpers; mod metrics; mod network; @@ -14,66 +17,176 @@ mod url_query; mod validator; use beacon_chain::{BeaconChain, BeaconChainTypes}; +use client_network::NetworkMessage; use client_network::Service as NetworkService; +use error::{ApiError, ApiResult}; use eth2_config::Eth2Config; +use futures::future::IntoFuture; use hyper::rt::Future; -use hyper::service::service_fn_ok; -use hyper::{Body, Method, Response, Server, StatusCode}; -use response_builder::ResponseBuilder; +use hyper::service::Service; +use hyper::{Body, Method, Request, Response, Server}; +use parking_lot::RwLock; use slog::{info, o, warn}; use std::ops::Deref; use std::path::PathBuf; use std::sync::Arc; use tokio::runtime::TaskExecutor; +use tokio::sync::mpsc; use url_query::UrlQuery; pub use beacon::{BlockResponse, HeadResponse, StateResponse}; pub use config::Config as ApiConfig; -#[derive(PartialEq, Debug)] -pub enum ApiError { - MethodNotAllowed(String), - ServerError(String), - NotImplemented(String), - InvalidQueryParams(String), - NotFound(String), - ImATeapot(String), // Just in case. +type BoxFut = Box, Error = ApiError> + Send>; + +pub struct ApiService { + log: slog::Logger, + beacon_chain: Arc>, + db_path: DBPath, + network_service: Arc>, + network_channel: Arc>>, + eth2_config: Arc, } -pub type ApiResult = Result, ApiError>; +fn into_boxfut(item: F) -> BoxFut +where + F: IntoFuture, Error = ApiError>, + F::Future: Send, +{ + Box::new(item.into_future()) +} -impl Into> for ApiError { - fn into(self) -> Response { - let status_code: (StatusCode, String) = match self { - ApiError::MethodNotAllowed(desc) => (StatusCode::METHOD_NOT_ALLOWED, desc), - ApiError::ServerError(desc) => (StatusCode::INTERNAL_SERVER_ERROR, desc), - ApiError::NotImplemented(desc) => (StatusCode::NOT_IMPLEMENTED, desc), - ApiError::InvalidQueryParams(desc) => (StatusCode::BAD_REQUEST, desc), - ApiError::NotFound(desc) => (StatusCode::NOT_FOUND, desc), - ApiError::ImATeapot(desc) => (StatusCode::IM_A_TEAPOT, desc), +impl Service for ApiService { + type ReqBody = Body; + type ResBody = Body; + type Error = ApiError; + type Future = BoxFut; + + fn call(&mut self, mut req: Request) -> Self::Future { + metrics::inc_counter(&metrics::REQUEST_COUNT); + let timer = metrics::start_timer(&metrics::REQUEST_RESPONSE_TIME); + + // Add all the useful bits into the request, so that we can pull them out in the individual + // functions. + req.extensions_mut() + .insert::(self.log.clone()); + req.extensions_mut() + .insert::>>(self.beacon_chain.clone()); + req.extensions_mut().insert::(self.db_path.clone()); + req.extensions_mut() + .insert::>>(self.network_service.clone()); + req.extensions_mut() + .insert::>>>( + self.network_channel.clone(), + ); + req.extensions_mut() + .insert::>(self.eth2_config.clone()); + + let path = req.uri().path().to_string(); + + // Route the request to the correct handler. + let result = match (req.method(), path.as_ref()) { + // Methods for Client + (&Method::GET, "/node/version") => into_boxfut(node::get_version(req)), + (&Method::GET, "/node/genesis_time") => into_boxfut(node::get_genesis_time::(req)), + (&Method::GET, "/node/syncing") => { + into_boxfut(helpers::implementation_pending_response(req)) + } + + // Methods for Network + (&Method::GET, "/network/enr") => into_boxfut(network::get_enr::(req)), + (&Method::GET, "/network/peer_count") => into_boxfut(network::get_peer_count::(req)), + (&Method::GET, "/network/peer_id") => into_boxfut(network::get_peer_id::(req)), + (&Method::GET, "/network/peers") => into_boxfut(network::get_peer_list::(req)), + (&Method::GET, "/network/listen_port") => { + into_boxfut(network::get_listen_port::(req)) + } + (&Method::GET, "/network/listen_addresses") => { + into_boxfut(network::get_listen_addresses::(req)) + } + + // Methods for Beacon Node + (&Method::GET, "/beacon/head") => into_boxfut(beacon::get_head::(req)), + (&Method::GET, "/beacon/block") => into_boxfut(beacon::get_block::(req)), + (&Method::GET, "/beacon/block_root") => into_boxfut(beacon::get_block_root::(req)), + (&Method::GET, "/beacon/blocks") => { + into_boxfut(helpers::implementation_pending_response(req)) + } + (&Method::GET, "/beacon/fork") => into_boxfut(beacon::get_fork::(req)), + (&Method::GET, "/beacon/attestations") => { + into_boxfut(helpers::implementation_pending_response(req)) + } + (&Method::GET, "/beacon/attestations/pending") => { + into_boxfut(helpers::implementation_pending_response(req)) + } + + (&Method::GET, "/beacon/validators") => into_boxfut(beacon::get_validators::(req)), + (&Method::GET, "/beacon/validators/indicies") => { + into_boxfut(helpers::implementation_pending_response(req)) + } + (&Method::GET, "/beacon/validators/pubkeys") => { + into_boxfut(helpers::implementation_pending_response(req)) + } + + // Methods for Validator + (&Method::GET, "/beacon/validator/duties") => { + into_boxfut(validator::get_validator_duties::(req)) + } + (&Method::GET, "/beacon/validator/block") => { + into_boxfut(validator::get_new_beacon_block::(req)) + } + (&Method::POST, "/beacon/validator/block") => validator::publish_beacon_block::(req), + (&Method::GET, "/beacon/validator/attestation") => { + into_boxfut(validator::get_new_attestation::(req)) + } + (&Method::POST, "/beacon/validator/attestation") => { + validator::publish_attestation::(req) + } + + (&Method::GET, "/beacon/state") => into_boxfut(beacon::get_state::(req)), + (&Method::GET, "/beacon/state_root") => into_boxfut(beacon::get_state_root::(req)), + (&Method::GET, "/beacon/state/current_finalized_checkpoint") => { + into_boxfut(beacon::get_current_finalized_checkpoint::(req)) + } + (&Method::GET, "/beacon/state/genesis") => { + into_boxfut(beacon::get_genesis_state::(req)) + } + //TODO: Add aggreggate/filtered state lookups here, e.g. /beacon/validators/balances + + // Methods for bootstrap and checking configuration + (&Method::GET, "/spec") => into_boxfut(spec::get_spec::(req)), + (&Method::GET, "/spec/slots_per_epoch") => { + into_boxfut(spec::get_slots_per_epoch::(req)) + } + (&Method::GET, "/spec/deposit_contract") => { + into_boxfut(helpers::implementation_pending_response(req)) + } + (&Method::GET, "/spec/eth2_config") => into_boxfut(spec::get_eth2_config::(req)), + + (&Method::GET, "/metrics") => into_boxfut(metrics::get_prometheus::(req)), + + _ => Box::new(futures::future::err(ApiError::NotFound( + "Request path and/or method not found.".to_owned(), + ))), }; - Response::builder() - .status(status_code.0) - .body(Body::from(status_code.1)) - .expect("Response should always be created.") - } -} -impl From for ApiError { - fn from(e: store::Error) -> ApiError { - ApiError::ServerError(format!("Database error: {:?}", e)) - } -} + let response = match result.wait() { + // Return the `hyper::Response`. + Ok(response) => { + metrics::inc_counter(&metrics::SUCCESS_COUNT); + slog::debug!(self.log, "Request successful: {:?}", path); + response + } + // Map the `ApiError` into `hyper::Response`. + Err(e) => { + slog::debug!(self.log, "Request failure: {:?}", path); + e.into() + } + }; -impl From for ApiError { - fn from(e: types::BeaconStateError) -> ApiError { - ApiError::ServerError(format!("BeaconState error: {:?}", e)) - } -} + metrics::stop_timer(timer); -impl From for ApiError { - fn from(e: state_processing::per_slot_processing::Error) -> ApiError { - ApiError::ServerError(format!("PerSlotProcessing error: {:?}", e)) + Box::new(futures::future::ok(response)) } } @@ -82,6 +195,7 @@ pub fn start_server( executor: &TaskExecutor, beacon_chain: Arc>, network_service: Arc>, + network_chan: mpsc::UnboundedSender, db_path: PathBuf, eth2_config: Eth2Config, log: &slog::Logger, @@ -107,108 +221,14 @@ pub fn start_server( let server_bc = beacon_chain.clone(); let eth2_config = Arc::new(eth2_config); - let service = move || { - let log = server_log.clone(); - let beacon_chain = server_bc.clone(); - let db_path = db_path.clone(); - let network_service = network_service.clone(); - let eth2_config = eth2_config.clone(); - - // Create a simple handler for the router, inject our stateful objects into the request. - service_fn_ok(move |mut req| { - metrics::inc_counter(&metrics::REQUEST_COUNT); - let timer = metrics::start_timer(&metrics::REQUEST_RESPONSE_TIME); - - req.extensions_mut().insert::(log.clone()); - req.extensions_mut() - .insert::>>(beacon_chain.clone()); - req.extensions_mut().insert::(db_path.clone()); - req.extensions_mut() - .insert::>>(network_service.clone()); - req.extensions_mut() - .insert::>(eth2_config.clone()); - - let path = req.uri().path().to_string(); - - // Route the request to the correct handler. - let result = match (req.method(), path.as_ref()) { - // Methods for Beacon Node - //TODO: Remove? - //(&Method::GET, "/beacon/best_slot") => beacon::get_best_slot::(req), - (&Method::GET, "/beacon/head") => beacon::get_head::(req), - (&Method::GET, "/beacon/block") => beacon::get_block::(req), - (&Method::GET, "/beacon/blocks") => helpers::implementation_pending_response(req), - //TODO Is the below replaced by finalized_checkpoint? - (&Method::GET, "/beacon/chainhead") => { - helpers::implementation_pending_response(req) - } - (&Method::GET, "/beacon/block_root") => beacon::get_block_root::(req), - (&Method::GET, "/beacon/latest_finalized_checkpoint") => { - beacon::get_latest_finalized_checkpoint::(req) - } - (&Method::GET, "/beacon/state") => beacon::get_state::(req), - (&Method::GET, "/beacon/state/genesis") => beacon::get_genesis_state::(req), - (&Method::GET, "/beacon/state_root") => beacon::get_state_root::(req), - - //TODO: Add aggreggate/filtered state lookups here, e.g. /beacon/validators/balances - - // Methods for Client - (&Method::GET, "/metrics") => metrics::get_prometheus::(req), - (&Method::GET, "/network/enr") => network::get_enr::(req), - (&Method::GET, "/network/peer_count") => network::get_peer_count::(req), - (&Method::GET, "/network/peer_id") => network::get_peer_id::(req), - (&Method::GET, "/network/peers") => network::get_peer_list::(req), - (&Method::GET, "/network/listen_port") => network::get_listen_port::(req), - (&Method::GET, "/network/listen_addresses") => { - network::get_listen_addresses::(req) - } - (&Method::GET, "/node/version") => node::get_version(req), - (&Method::GET, "/node/genesis_time") => node::get_genesis_time::(req), - (&Method::GET, "/node/deposit_contract") => { - helpers::implementation_pending_response(req) - } - (&Method::GET, "/node/syncing") => helpers::implementation_pending_response(req), - (&Method::GET, "/node/fork") => helpers::implementation_pending_response(req), - - // Methods for Validator - (&Method::GET, "/validator/duties") => validator::get_validator_duties::(req), - (&Method::GET, "/validator/block") => helpers::implementation_pending_response(req), - (&Method::POST, "/validator/block") => { - helpers::implementation_pending_response(req) - } - (&Method::GET, "/validator/attestation") => { - helpers::implementation_pending_response(req) - } - (&Method::POST, "/validator/attestation") => { - helpers::implementation_pending_response(req) - } - - (&Method::GET, "/spec") => spec::get_spec::(req), - (&Method::GET, "/spec/slots_per_epoch") => spec::get_slots_per_epoch::(req), - (&Method::GET, "/spec/eth2_config") => spec::get_eth2_config::(req), - - _ => Err(ApiError::NotFound( - "Request path and/or method not found.".to_owned(), - )), - }; - - let response = match result { - // Return the `hyper::Response`. - Ok(response) => { - metrics::inc_counter(&metrics::SUCCESS_COUNT); - slog::debug!(log, "Request successful: {:?}", path); - response - } - // Map the `ApiError` into `hyper::Response`. - Err(e) => { - slog::debug!(log, "Request failure: {:?}", path); - e.into() - } - }; - - metrics::stop_timer(timer); - - response + let service = move || -> futures::future::FutureResult, String> { + futures::future::ok(ApiService { + log: server_log.clone(), + beacon_chain: server_bc.clone(), + db_path: db_path.clone(), + network_service: network_service.clone(), + network_channel: Arc::new(RwLock::new(network_chan.clone())), + eth2_config: eth2_config.clone(), }) }; @@ -218,16 +238,16 @@ pub fn start_server( .with_graceful_shutdown(server_exit) .map_err(move |e| { warn!( - log_clone, - "API failed to start, Unable to bind"; "address" => format!("{:?}", e) + log_clone, + "API failed to start, Unable to bind"; "address" => format!("{:?}", e) ) }); info!( - log, - "REST API started"; - "address" => format!("{}", config.listen_address), - "port" => config.port, + log, + "REST API started"; + "address" => format!("{}", config.listen_address), + "port" => config.port, ); executor.spawn(server); @@ -235,13 +255,6 @@ pub fn start_server( Ok(exit_signal) } -fn success_response(body: Body) -> Response { - Response::builder() - .status(StatusCode::OK) - .body(body) - .expect("We should always be able to make response from the success body.") -} - #[derive(Clone)] pub struct DBPath(PathBuf); diff --git a/beacon_node/rest_api/src/macros.rs b/beacon_node/rest_api/src/macros.rs new file mode 100644 index 000000000..e95cfb8ae --- /dev/null +++ b/beacon_node/rest_api/src/macros.rs @@ -0,0 +1,13 @@ +macro_rules! try_future { + ($expr:expr) => { + match $expr { + core::result::Result::Ok(val) => val, + core::result::Result::Err(err) => { + return Box::new(futures::future::err(std::convert::From::from(err))) + } + } + }; + ($expr:expr,) => { + $crate::try_future!($expr) + }; +} diff --git a/beacon_node/rest_api/src/metrics.rs b/beacon_node/rest_api/src/metrics.rs index 064359337..e9d98434e 100644 --- a/beacon_node/rest_api/src/metrics.rs +++ b/beacon_node/rest_api/src/metrics.rs @@ -1,8 +1,9 @@ -use crate::{success_response, ApiError, ApiResult, DBPath}; -use beacon_chain::{BeaconChain, BeaconChainTypes}; +use crate::helpers::get_beacon_chain_from_request; +use crate::response_builder::ResponseBuilder; +use crate::{ApiError, ApiResult, DBPath}; +use beacon_chain::BeaconChainTypes; use hyper::{Body, Request}; use prometheus::{Encoder, TextEncoder}; -use std::sync::Arc; pub use lighthouse_metrics::*; @@ -30,10 +31,7 @@ pub fn get_prometheus(req: Request) -> ApiR let mut buffer = vec![]; let encoder = TextEncoder::new(); - let beacon_chain = req - .extensions() - .get::>>() - .ok_or_else(|| ApiError::ServerError("Beacon chain extension missing".to_string()))?; + let beacon_chain = get_beacon_chain_from_request::(&req)?; let db_path = req .extensions() .get::() @@ -64,6 +62,6 @@ pub fn get_prometheus(req: Request) -> ApiR .unwrap(); String::from_utf8(buffer) - .map(|string| success_response(Body::from(string))) - .map_err(|e| ApiError::ServerError(format!("Failed to encode prometheus info: {:?}", e))) + .map(|string| ResponseBuilder::new(&req)?.body_text(string)) + .map_err(|e| ApiError::ServerError(format!("Failed to encode prometheus info: {:?}", e)))? } diff --git a/beacon_node/rest_api/src/network.rs b/beacon_node/rest_api/src/network.rs index a3e4c5ee7..f193ef8ea 100644 --- a/beacon_node/rest_api/src/network.rs +++ b/beacon_node/rest_api/src/network.rs @@ -1,108 +1,78 @@ -use crate::{success_response, ApiError, ApiResult, NetworkService}; +use crate::error::ApiResult; +use crate::response_builder::ResponseBuilder; +use crate::NetworkService; use beacon_chain::BeaconChainTypes; -use eth2_libp2p::{Enr, Multiaddr, PeerId}; +use eth2_libp2p::{Multiaddr, PeerId}; use hyper::{Body, Request}; use std::sync::Arc; -/// HTTP handle to return the list of libp2p multiaddr the client is listening on. +/// HTTP handler to return the list of libp2p multiaddr the client is listening on. /// /// Returns a list of `Multiaddr`, serialized according to their `serde` impl. pub fn get_listen_addresses(req: Request) -> ApiResult { let network = req .extensions() .get::>>() - .ok_or_else(|| ApiError::ServerError("NetworkService extension missing".to_string()))?; - + .expect("The network service should always be there, we put it there"); let multiaddresses: Vec = network.listen_multiaddrs(); - - Ok(success_response(Body::from( - serde_json::to_string(&multiaddresses) - .map_err(|e| ApiError::ServerError(format!("Unable to serialize Enr: {:?}", e)))?, - ))) + ResponseBuilder::new(&req)?.body_no_ssz(&multiaddresses) } -/// HTTP handle to return the list of libp2p multiaddr the client is listening on. +/// HTTP handler to return the network port the client is listening on. /// -/// Returns a list of `Multiaddr`, serialized according to their `serde` impl. +/// Returns the TCP port number in its plain form (which is also valid JSON serialization) pub fn get_listen_port(req: Request) -> ApiResult { let network = req .extensions() .get::>>() - .ok_or_else(|| ApiError::ServerError("NetworkService extension missing".to_string()))?; - - Ok(success_response(Body::from( - serde_json::to_string(&network.listen_port()) - .map_err(|e| ApiError::ServerError(format!("Unable to serialize port: {:?}", e)))?, - ))) + .expect("The network service should always be there, we put it there") + .clone(); + ResponseBuilder::new(&req)?.body(&network.listen_port()) } -/// HTTP handle to return the Discv5 ENR from the client's libp2p service. +/// HTTP handler to return the Discv5 ENR from the client's libp2p service. /// /// ENR is encoded as base64 string. pub fn get_enr(req: Request) -> ApiResult { let network = req .extensions() .get::>>() - .ok_or_else(|| ApiError::ServerError("NetworkService extension missing".to_string()))?; - - let enr: Enr = network.local_enr(); - - Ok(success_response(Body::from( - serde_json::to_string(&enr.to_base64()) - .map_err(|e| ApiError::ServerError(format!("Unable to serialize Enr: {:?}", e)))?, - ))) + .expect("The network service should always be there, we put it there"); + ResponseBuilder::new(&req)?.body_no_ssz(&network.local_enr().to_base64()) } -/// HTTP handle to return the `PeerId` from the client's libp2p service. +/// HTTP handler to return the `PeerId` from the client's libp2p service. /// /// PeerId is encoded as base58 string. pub fn get_peer_id(req: Request) -> ApiResult { let network = req .extensions() .get::>>() - .ok_or_else(|| ApiError::ServerError("NetworkService extension missing".to_string()))?; - - let peer_id: PeerId = network.local_peer_id(); - - Ok(success_response(Body::from( - serde_json::to_string(&peer_id.to_base58()) - .map_err(|e| ApiError::ServerError(format!("Unable to serialize Enr: {:?}", e)))?, - ))) + .expect("The network service should always be there, we put it there"); + ResponseBuilder::new(&req)?.body_no_ssz(&network.local_peer_id().to_base58()) } -/// HTTP handle to return the number of peers connected in the client's libp2p service. +/// HTTP handler to return the number of peers connected in the client's libp2p service. pub fn get_peer_count(req: Request) -> ApiResult { let network = req .extensions() .get::>>() - .ok_or_else(|| ApiError::ServerError("NetworkService extension missing".to_string()))?; - - let connected_peers: usize = network.connected_peers(); - - Ok(success_response(Body::from( - serde_json::to_string(&connected_peers) - .map_err(|e| ApiError::ServerError(format!("Unable to serialize Enr: {:?}", e)))?, - ))) + .expect("The network service should always be there, we put it there"); + ResponseBuilder::new(&req)?.body(&network.connected_peers()) } -/// HTTP handle to return the list of peers connected to the client's libp2p service. +/// HTTP handler to return the list of peers connected to the client's libp2p service. /// /// Peers are presented as a list of `PeerId::to_string()`. pub fn get_peer_list(req: Request) -> ApiResult { let network = req .extensions() .get::>>() - .ok_or_else(|| ApiError::ServerError("NetworkService extension missing".to_string()))?; - + .expect("The network service should always be there, we put it there"); let connected_peers: Vec = network .connected_peer_set() .iter() .map(PeerId::to_string) .collect(); - - Ok(success_response(Body::from( - serde_json::to_string(&connected_peers).map_err(|e| { - ApiError::ServerError(format!("Unable to serialize Vec: {:?}", e)) - })?, - ))) + ResponseBuilder::new(&req)?.body_no_ssz(&connected_peers) } diff --git a/beacon_node/rest_api/src/node.rs b/beacon_node/rest_api/src/node.rs index 4dbd41229..882edcfd5 100644 --- a/beacon_node/rest_api/src/node.rs +++ b/beacon_node/rest_api/src/node.rs @@ -1,25 +1,17 @@ -use crate::{success_response, ApiResult}; -use beacon_chain::{BeaconChain, BeaconChainTypes}; +use crate::helpers::get_beacon_chain_from_request; +use crate::response_builder::ResponseBuilder; +use crate::ApiResult; +use beacon_chain::BeaconChainTypes; use hyper::{Body, Request}; -use std::sync::Arc; use version; /// Read the version string from the current Lighthouse build. -pub fn get_version(_req: Request) -> ApiResult { - let body = Body::from( - serde_json::to_string(&version::version()) - .expect("Version should always be serialializable as JSON."), - ); - Ok(success_response(body)) +pub fn get_version(req: Request) -> ApiResult { + ResponseBuilder::new(&req)?.body_no_ssz(&version::version()) } /// Read the genesis time from the current beacon chain state. pub fn get_genesis_time(req: Request) -> ApiResult { - let beacon_chain = req.extensions().get::>>().unwrap(); - let gen_time: u64 = beacon_chain.head().beacon_state.genesis_time; - let body = Body::from( - serde_json::to_string(&gen_time) - .expect("Genesis should time always have a valid JSON serialization."), - ); - Ok(success_response(body)) + let beacon_chain = get_beacon_chain_from_request::(&req)?; + ResponseBuilder::new(&req)?.body(&beacon_chain.head().beacon_state.genesis_time) } diff --git a/beacon_node/rest_api/src/response_builder.rs b/beacon_node/rest_api/src/response_builder.rs index 9b8819996..d5b530f8a 100644 --- a/beacon_node/rest_api/src/response_builder.rs +++ b/beacon_node/rest_api/src/response_builder.rs @@ -8,6 +8,7 @@ pub enum Encoding { JSON, SSZ, YAML, + TEXT, } pub struct ResponseBuilder { @@ -15,36 +16,84 @@ pub struct ResponseBuilder { } impl ResponseBuilder { - pub fn new(req: &Request) -> Self { - let encoding = match req.headers().get(header::CONTENT_TYPE) { - Some(h) if h == "application/ssz" => Encoding::SSZ, - Some(h) if h == "application/yaml" => Encoding::YAML, + pub fn new(req: &Request) -> Result { + let content_header: String = req + .headers() + .get(header::CONTENT_TYPE) + .map_or(Ok(""), |h| h.to_str()) + .map_err(|e| { + ApiError::BadRequest(format!( + "The content-type header contains invalid characters: {:?}", + e + )) + }) + .map(|h| String::from(h))?; + + // JSON is our default encoding, unless something else is requested. + let encoding = match content_header { + ref h if h.starts_with("application/ssz") => Encoding::SSZ, + ref h if h.starts_with("application/yaml") => Encoding::YAML, + ref h if h.starts_with("text/") => Encoding::TEXT, _ => Encoding::JSON, }; - - Self { encoding } + Ok(Self { encoding }) } pub fn body(self, item: &T) -> ApiResult { - let body: Body = match self.encoding { - Encoding::JSON => Body::from(serde_json::to_string(&item).map_err(|e| { - ApiError::ServerError(format!( - "Unable to serialize response body as JSON: {:?}", - e - )) - })?), - Encoding::SSZ => Body::from(item.as_ssz_bytes()), - Encoding::YAML => Body::from(serde_yaml::to_string(&item).map_err(|e| { - ApiError::ServerError(format!( - "Unable to serialize response body as YAML: {:?}", - e - )) - })?), + match self.encoding { + Encoding::SSZ => Response::builder() + .status(StatusCode::OK) + .header("content-type", "application/ssz") + .body(Body::from(item.as_ssz_bytes())) + .map_err(|e| ApiError::ServerError(format!("Failed to build response: {:?}", e))), + _ => self.body_no_ssz(item), + } + } + + pub fn body_no_ssz(self, item: &T) -> ApiResult { + let (body, content_type) = match self.encoding { + Encoding::JSON => ( + Body::from(serde_json::to_string(&item).map_err(|e| { + ApiError::ServerError(format!( + "Unable to serialize response body as JSON: {:?}", + e + )) + })?), + "application/json", + ), + Encoding::SSZ => { + return Err(ApiError::UnsupportedType( + "Response cannot be encoded as SSZ.".into(), + )); + } + Encoding::YAML => ( + Body::from(serde_yaml::to_string(&item).map_err(|e| { + ApiError::ServerError(format!( + "Unable to serialize response body as YAML: {:?}", + e + )) + })?), + "application/yaml", + ), + Encoding::TEXT => { + return Err(ApiError::UnsupportedType( + "Response cannot be encoded as plain text.".into(), + )); + } }; Response::builder() .status(StatusCode::OK) + .header("content-type", content_type) .body(Body::from(body)) .map_err(|e| ApiError::ServerError(format!("Failed to build response: {:?}", e))) } + + pub fn body_text(self, text: String) -> ApiResult { + Response::builder() + .status(StatusCode::OK) + .header("content-type", "text/plain; charset=utf-8") + .body(Body::from(text)) + .map_err(|e| ApiError::ServerError(format!("Failed to build response: {:?}", e))) + } } diff --git a/beacon_node/rest_api/src/spec.rs b/beacon_node/rest_api/src/spec.rs index 86d1c227d..083ff5ad4 100644 --- a/beacon_node/rest_api/src/spec.rs +++ b/beacon_node/rest_api/src/spec.rs @@ -1,6 +1,8 @@ -use super::{success_response, ApiResult}; +use super::ApiResult; +use crate::helpers::get_beacon_chain_from_request; +use crate::response_builder::ResponseBuilder; use crate::ApiError; -use beacon_chain::{BeaconChain, BeaconChainTypes}; +use beacon_chain::BeaconChainTypes; use eth2_config::Eth2Config; use hyper::{Body, Request}; use std::sync::Arc; @@ -8,15 +10,8 @@ use types::EthSpec; /// HTTP handler to return the full spec object. pub fn get_spec(req: Request) -> ApiResult { - let beacon_chain = req - .extensions() - .get::>>() - .ok_or_else(|| ApiError::ServerError("Beacon chain extension missing".to_string()))?; - - let json: String = serde_json::to_string(&beacon_chain.spec) - .map_err(|e| ApiError::ServerError(format!("Unable to serialize spec: {:?}", e)))?; - - Ok(success_response(Body::from(json))) + let beacon_chain = get_beacon_chain_from_request::(&req)?; + ResponseBuilder::new(&req)?.body_no_ssz(&beacon_chain.spec) } /// HTTP handler to return the full Eth2Config object. @@ -26,16 +21,10 @@ pub fn get_eth2_config(req: Request) -> Api .get::>() .ok_or_else(|| ApiError::ServerError("Eth2Config extension missing".to_string()))?; - let json: String = serde_json::to_string(eth2_config.as_ref()) - .map_err(|e| ApiError::ServerError(format!("Unable to serialize Eth2Config: {:?}", e)))?; - - Ok(success_response(Body::from(json))) + ResponseBuilder::new(&req)?.body_no_ssz(eth2_config.as_ref()) } /// HTTP handler to return the full spec object. -pub fn get_slots_per_epoch(_req: Request) -> ApiResult { - let json: String = serde_json::to_string(&T::EthSpec::slots_per_epoch()) - .map_err(|e| ApiError::ServerError(format!("Unable to serialize epoch: {:?}", e)))?; - - Ok(success_response(Body::from(json))) +pub fn get_slots_per_epoch(req: Request) -> ApiResult { + ResponseBuilder::new(&req)?.body(&T::EthSpec::slots_per_epoch()) } diff --git a/beacon_node/rest_api/src/url_query.rs b/beacon_node/rest_api/src/url_query.rs index 3802ff831..f0c587a32 100644 --- a/beacon_node/rest_api/src/url_query.rs +++ b/beacon_node/rest_api/src/url_query.rs @@ -12,7 +12,7 @@ impl<'a> UrlQuery<'a> { /// Returns `Err` if `req` does not contain any query parameters. pub fn from_request(req: &'a Request) -> Result { let query_str = req.uri().query().ok_or_else(|| { - ApiError::InvalidQueryParams( + ApiError::BadRequest( "URL query must be valid and contain at least one key.".to_string(), ) })?; @@ -28,7 +28,7 @@ impl<'a> UrlQuery<'a> { .find(|(key, _value)| keys.contains(&&**key)) .map(|(key, value)| (key.into_owned(), value.into_owned())) .ok_or_else(|| { - ApiError::InvalidQueryParams(format!( + ApiError::BadRequest(format!( "URL query must contain at least one of the following keys: {:?}", keys )) @@ -48,13 +48,13 @@ impl<'a> UrlQuery<'a> { if first_key == key { Ok(first_value.to_string()) } else { - Err(ApiError::InvalidQueryParams(format!( + Err(ApiError::BadRequest(format!( "Only the {} query parameter is supported", key ))) } } else { - Err(ApiError::InvalidQueryParams(format!( + Err(ApiError::BadRequest(format!( "Only one query parameter is allowed, {} supplied", queries.len() ))) diff --git a/beacon_node/rest_api/src/validator.rs b/beacon_node/rest_api/src/validator.rs index 0440a7368..60c0eed06 100644 --- a/beacon_node/rest_api/src/validator.rs +++ b/beacon_node/rest_api/src/validator.rs @@ -1,12 +1,23 @@ -use super::{success_response, ApiResult}; -use crate::{helpers::*, ApiError, UrlQuery}; -use beacon_chain::{BeaconChain, BeaconChainTypes}; -use bls::PublicKey; +use crate::helpers::{ + check_content_type_for_json, get_beacon_chain_from_request, get_logger_from_request, + parse_pubkey, publish_attestation_to_network, publish_beacon_block_to_network, +}; +use crate::response_builder::ResponseBuilder; +use crate::{ApiError, ApiResult, BoxFut, UrlQuery}; +use beacon_chain::{AttestationProcessingOutcome, BeaconChainTypes, BlockProcessingOutcome}; +use bls::{AggregateSignature, PublicKey, Signature}; +use futures::future::Future; +use futures::stream::Stream; use hyper::{Body, Request}; +use network::NetworkMessage; +use parking_lot::RwLock; use serde::{Deserialize, Serialize}; +use slog::{info, trace, warn}; use std::sync::Arc; +use tokio; +use tokio::sync::mpsc; use types::beacon_state::EthSpec; -use types::{Epoch, RelativeEpoch, Shard, Slot}; +use types::{Attestation, BeaconBlock, BitList, Epoch, RelativeEpoch, Shard, Slot}; #[derive(Debug, Serialize, Deserialize)] pub struct ValidatorDuty { @@ -33,44 +44,47 @@ impl ValidatorDuty { /// HTTP Handler to retrieve a the duties for a set of validators during a particular epoch pub fn get_validator_duties(req: Request) -> ApiResult { - // Get beacon state - let beacon_chain = req - .extensions() - .get::>>() - .ok_or_else(|| ApiError::ServerError("Beacon chain extension missing".to_string()))?; - let head_state = &beacon_chain.head().beacon_state; + let log = get_logger_from_request(&req); + slog::trace!(log, "Validator duties requested of API: {:?}", &req); + let beacon_chain = get_beacon_chain_from_request::(&req)?; + let mut head_state = beacon_chain.head().beacon_state; + slog::trace!(log, "Got head state from request."); // Parse and check query parameters let query = UrlQuery::from_request(&req)?; - let current_epoch = head_state.current_epoch(); let epoch = match query.first_of(&["epoch"]) { - Ok((_, v)) => Epoch::new(v.parse::().map_err(|e| { - ApiError::InvalidQueryParams(format!("Invalid epoch parameter, must be a u64. {:?}", e)) - })?), + Ok((_, v)) => { + slog::trace!(log, "Requested epoch {:?}", v); + Epoch::new(v.parse::().map_err(|e| { + slog::info!(log, "Invalid epoch {:?}", e); + ApiError::BadRequest(format!("Invalid epoch parameter, must be a u64. {:?}", e)) + })?) + } Err(_) => { // epoch not supplied, use the current epoch + slog::info!(log, "Using default epoch {:?}", current_epoch); current_epoch } }; let relative_epoch = RelativeEpoch::from_epoch(current_epoch, epoch).map_err(|e| { - ApiError::InvalidQueryParams(format!( + slog::info!(log, "Requested epoch out of range."); + ApiError::BadRequest(format!( "Cannot get RelativeEpoch, epoch out of range: {:?}", e )) })?; - //TODO: Handle an array of validators, currently only takes one - let validators: Vec = match query.all_of("validator_pubkeys") { - Ok(v) => v - .iter() - .map(|pk| parse_pubkey(pk)) - .collect::, _>>()?, - Err(e) => { - return Err(e); - } - }; + let validators: Vec = query + .all_of("validator_pubkeys")? + .iter() + .map(|pk| parse_pubkey(pk)) + .collect::, _>>()?; let mut duties: Vec = Vec::new(); + // Build cache for the requested epoch + head_state + .build_committee_cache(relative_epoch, &beacon_chain.spec) + .map_err(|e| ApiError::ServerError(format!("Unable to build committee cache: {:?}", e)))?; // Get a list of all validators for this epoch let validator_proposers: Vec = epoch .slot_iter(T::EthSpec::slots_per_epoch()) @@ -135,9 +149,263 @@ pub fn get_validator_duties(req: Request) - duties.append(&mut vec![duty]); } - let body = Body::from( - serde_json::to_string(&duties) - .expect("We should always be able to serialize the duties we created."), - ); - Ok(success_response(body)) + ResponseBuilder::new(&req)?.body_no_ssz(&duties) +} + +/// HTTP Handler to produce a new BeaconBlock from the current state, ready to be signed by a validator. +pub fn get_new_beacon_block(req: Request) -> ApiResult { + let beacon_chain = get_beacon_chain_from_request::(&req)?; + + let query = UrlQuery::from_request(&req)?; + let slot = query + .first_of(&["slot"]) + .map(|(_key, value)| value)? + .parse::() + .map(Slot::from) + .map_err(|e| { + ApiError::BadRequest(format!("Invalid slot parameter, must be a u64. {:?}", e)) + })?; + let randao_bytes = query + .first_of(&["randao_reveal"]) + .map(|(_key, value)| value) + .map(hex::decode)? + .map_err(|e| { + ApiError::BadRequest(format!("Invalid hex string for randao_reveal: {:?}", e)) + })?; + let randao_reveal = Signature::from_bytes(randao_bytes.as_slice()).map_err(|e| { + ApiError::BadRequest(format!("randao_reveal is not a valid signature: {:?}", e)) + })?; + + let (new_block, _state) = beacon_chain + .produce_block(randao_reveal, slot) + .map_err(|e| { + ApiError::ServerError(format!( + "Beacon node is not able to produce a block: {:?}", + e + )) + })?; + + ResponseBuilder::new(&req)?.body(&new_block) +} + +/// HTTP Handler to publish a BeaconBlock, which has been signed by a validator. +pub fn publish_beacon_block(req: Request) -> BoxFut { + let _ = try_future!(check_content_type_for_json(&req)); + let log = get_logger_from_request(&req); + let beacon_chain = try_future!(get_beacon_chain_from_request::(&req)); + // Get the network sending channel from the request, for later transmission + let network_chan = req + .extensions() + .get::>>>() + .expect("Should always get the network channel from the request, since we put it in there.") + .clone(); + + let response_builder = ResponseBuilder::new(&req); + + let body = req.into_body(); + trace!( + log, + "Got the request body, now going to parse it into a block." + ); + Box::new(body + .concat2() + .map_err(|e| ApiError::ServerError(format!("Unable to get request body: {:?}",e))) + .map(|chunk| chunk.iter().cloned().collect::>()) + .and_then(|chunks| { + serde_json::from_slice(&chunks.as_slice()).map_err(|e| { + ApiError::BadRequest(format!( + "Unable to deserialize JSON into a BeaconBlock: {:?}", + e + )) + }) + }) + .and_then(move |block: BeaconBlock| { + let slot = block.slot; + match beacon_chain.process_block(block.clone()) { + Ok(BlockProcessingOutcome::Processed { block_root }) => { + // Block was processed, publish via gossipsub + info!(log, "Processed valid block from API, transmitting to network."; "block_slot" => slot, "block_root" => format!("{}", block_root)); + publish_beacon_block_to_network::(network_chan, block) + } + Ok(outcome) => { + warn!(log, "BeaconBlock could not be processed, but is being sent to the network anyway."; "outcome" => format!("{:?}", outcome)); + publish_beacon_block_to_network::(network_chan, block)?; + Err(ApiError::ProcessingError(format!( + "The BeaconBlock could not be processed, but has still been published: {:?}", + outcome + ))) + } + Err(e) => { + Err(ApiError::ServerError(format!( + "Error while processing block: {:?}", + e + ))) + } + } + }).and_then(|_| { + response_builder?.body_no_ssz(&()) + })) +} + +/// HTTP Handler to produce a new Attestation from the current state, ready to be signed by a validator. +pub fn get_new_attestation(req: Request) -> ApiResult { + let beacon_chain = get_beacon_chain_from_request::(&req)?; + let mut head_state = beacon_chain.head().beacon_state; + + let query = UrlQuery::from_request(&req)?; + let val_pk_str = query + .first_of(&["validator_pubkey"]) + .map(|(_key, value)| value)?; + let val_pk = parse_pubkey(val_pk_str.as_str())?; + + head_state + .update_pubkey_cache() + .map_err(|e| ApiError::ServerError(format!("Unable to build pubkey cache: {:?}", e)))?; + // Get the validator index from the supplied public key + // If it does not exist in the index, we cannot continue. + let val_index = head_state + .get_validator_index(&val_pk) + .map_err(|e| { + ApiError::ServerError(format!("Unable to read validator index cache. {:?}", e)) + })? + .ok_or(ApiError::BadRequest( + "The provided validator public key does not correspond to a validator index.".into(), + ))?; + + // Build cache for the requested epoch + head_state + .build_committee_cache(RelativeEpoch::Current, &beacon_chain.spec) + .map_err(|e| ApiError::ServerError(format!("Unable to build committee cache: {:?}", e)))?; + // Get the duties of the validator, to make sure they match up. + // If they don't have duties this epoch, then return an error + let val_duty = head_state + .get_attestation_duties(val_index, RelativeEpoch::Current) + .map_err(|e| { + ApiError::ServerError(format!( + "unable to read cache for attestation duties: {:?}", + e + )) + })? + .ok_or(ApiError::BadRequest("No validator duties could be found for the requested validator. Cannot provide valid attestation.".into()))?; + + // Check that we are requesting an attestation during the slot where it is relevant. + let present_slot = beacon_chain.slot().map_err(|e| ApiError::ServerError( + format!("Beacon node is unable to determine present slot, either the state isn't generated or the chain hasn't begun. {:?}", e) + ))?; + if val_duty.slot != present_slot { + return Err(ApiError::BadRequest(format!("Validator is only able to request an attestation during the slot they are allocated. Current slot: {:?}, allocated slot: {:?}", head_state.slot, val_duty.slot))); + } + + // Parse the POC bit and insert it into the aggregation bits + let poc_bit = query + .first_of(&["poc_bit"]) + .map(|(_key, value)| value)? + .parse::() + .map_err(|e| { + ApiError::BadRequest(format!("Invalid slot parameter, must be a u64. {:?}", e)) + })?; + + let mut aggregation_bits = BitList::with_capacity(val_duty.committee_len) + .expect("An empty BitList should always be created, or we have bigger problems."); + aggregation_bits + .set(val_duty.committee_index, poc_bit) + .map_err(|e| { + ApiError::ServerError(format!( + "Unable to set aggregation bits for the attestation: {:?}", + e + )) + })?; + + // Allow a provided slot parameter to check against the expected slot as a sanity check only. + // Presently, we don't support attestations at future or past slots. + let requested_slot = query + .first_of(&["slot"]) + .map(|(_key, value)| value)? + .parse::() + .map(Slot::from) + .map_err(|e| { + ApiError::BadRequest(format!("Invalid slot parameter, must be a u64. {:?}", e)) + })?; + let current_slot = beacon_chain.head().beacon_state.slot.as_u64(); + if requested_slot != current_slot { + return Err(ApiError::BadRequest(format!("Attestation data can only be requested for the current slot ({:?}), not your requested slot ({:?})", current_slot, requested_slot))); + } + + let shard = query + .first_of(&["shard"]) + .map(|(_key, value)| value)? + .parse::() + .map_err(|e| ApiError::BadRequest(format!("Shard is not a valid u64 value: {:?}", e)))?; + + let attestation_data = beacon_chain + .produce_attestation_data(shard, current_slot.into()) + .map_err(|e| ApiError::ServerError(format!("Could not produce an attestation: {:?}", e)))?; + + let attestation: Attestation = Attestation { + aggregation_bits, + data: attestation_data, + custody_bits: BitList::with_capacity(val_duty.committee_len) + .expect("Should be able to create an empty BitList for the custody bits."), + signature: AggregateSignature::new(), + }; + + ResponseBuilder::new(&req)?.body(&attestation) +} + +/// HTTP Handler to publish an Attestation, which has been signed by a validator. +pub fn publish_attestation(req: Request) -> BoxFut { + let _ = try_future!(check_content_type_for_json(&req)); + let log = get_logger_from_request(&req); + let beacon_chain = try_future!(get_beacon_chain_from_request::(&req)); + // Get the network sending channel from the request, for later transmission + let network_chan = req + .extensions() + .get::>>>() + .expect("Should always get the network channel from the request, since we put it in there.") + .clone(); + + let response_builder = ResponseBuilder::new(&req); + + let body = req.into_body(); + trace!( + log, + "Got the request body, now going to parse it into an attesation." + ); + Box::new(body + .concat2() + .map_err(|e| ApiError::ServerError(format!("Unable to get request body: {:?}",e))) + .map(|chunk| chunk.iter().cloned().collect::>()) + .and_then(|chunks| { + serde_json::from_slice(&chunks.as_slice()).map_err(|e| { + ApiError::BadRequest(format!( + "Unable to deserialize JSON into a BeaconBlock: {:?}", + e + )) + }) + }) + .and_then(move |attestation: Attestation| { + match beacon_chain.process_attestation(attestation.clone()) { + Ok(AttestationProcessingOutcome::Processed) => { + // Block was processed, publish via gossipsub + info!(log, "Processed valid attestation from API, transmitting to network."); + publish_attestation_to_network::(network_chan, attestation) + } + Ok(outcome) => { + warn!(log, "Attestation could not be processed, but is being sent to the network anyway."; "outcome" => format!("{:?}", outcome)); + publish_attestation_to_network::(network_chan, attestation)?; + Err(ApiError::ProcessingError(format!( + "The Attestation could not be processed, but has still been published: {:?}", + outcome + ))) + } + Err(e) => { + Err(ApiError::ServerError(format!( + "Error while processing attestation: {:?}", + e + ))) + } + } + }).and_then(|_| { + response_builder?.body_no_ssz(&()) + })) } diff --git a/docs/rest_oapi.yaml b/docs/api_spec.yaml similarity index 59% rename from docs/rest_oapi.yaml rename to docs/api_spec.yaml index dea892c18..23608807e 100644 --- a/docs/rest_oapi.yaml +++ b/docs/api_spec.yaml @@ -2,7 +2,7 @@ openapi: "3.0.2" info: title: "Lighthouse REST API" description: "" - version: "0.1.0" + version: "0.2.0" license: name: "Apache 2.0" url: "https://www.apache.org/licenses/LICENSE-2.0.html" @@ -13,6 +13,8 @@ tags: description: Endpoints which will be implemented for phase 1 of Ethereum Serenity - name: Future description: Potential future endpoints or optional nice-to-haves + - name: RFC + description: Do we need these endpoints at all? This is a request for comments if you think they're useful. paths: /node/version: @@ -47,21 +49,6 @@ paths: 500: $ref: '#/components/responses/InternalError' - /node/deposit_contract: - get: - tags: - - Phase0 - summary: "Get the address of the Ethereum 1 deposit contract." - description: "Requests the address of the deposit contract on the Ethereum 1 chain, which was used to start the current beacon chain." - responses: - 200: - description: Request successful - content: - application/json: - schema: - $ref: '#/components/schemas/ethereum_address' - 500: - $ref: '#/components/responses/InternalError' /node/syncing: get: @@ -85,55 +72,23 @@ paths: 500: $ref: '#/components/responses/InternalError' - /node/fork: + /network/enr: get: tags: - Phase0 - summary: "Get fork information from running beacon node." - description: "Requests the beacon node to provide which fork version it is currently on." + summary: "Get the node's Ethereum Node Record (ENR)." + description: "The Ethereum Node Record (ENR) contains a compressed public key, an IPv4 address, a TCP port and a UDP port, which is all encoded using base64. This endpoint fetches the base64 encoded version of the ENR for the running beacon node." responses: 200: description: Request successful content: application/json: schema: - type: object - properties: - fork: - $ref: '#/components/schemas/Fork' - chain_id: - type: integer - format: uint64 - description: "Sometimes called the network id, this number discerns the active chain for the beacon node. Analogous to Eth1.0 JSON-RPC net_version." + $ref: '#/components/schemas/ENR' 500: $ref: '#/components/responses/InternalError' - /node/stats: - get: - tags: - - Future - summary: "Get operational information about the node." - description: "Fetches some operational information about the node's process, such as memory usage, database size, etc." - responses: - 200: - description: Request successful - content: - application/json: - schema: - type: object - properties: - memory_usage: - type: integer - format: uint64 - description: "The amount of memory used by the currently running beacon node process, expressed in bytes." - uptime: - type: integer - format: uint64 - description: "The number of seconds that have elapsed since beacon node process was started." - #TODO: what other useful process information could be expressed here? - - - /node/network/peer_count: + /network/peer_count: get: tags: - Phase0 @@ -148,8 +103,29 @@ paths: type: integer format: uint64 example: 25 + 500: + $ref: '#/components/responses/InternalError' - /node/network/peers: + /network/peer_id: + get: + tags: + - Phase0 + summary: "Get the node's libp2p peer ID." + description: "Requests the node to provide it's libp2p ['peer ID'](https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md), which is a base58 encoded SHA2-256 'multihash' of the node's public key struct." + responses: + 200: + description: Request successful + content: + application/json: + schema: + type: string + format: byte + example: "QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N" + pattern: "^[1-9A-HJ-NP-Za-km-z]{46}$" + 500: + $ref: '#/components/responses/InternalError' + + /network/peers: get: tags: - Phase0 @@ -164,32 +140,49 @@ paths: type: array items: $ref: '#/components/schemas/Peer' + 500: + $ref: '#/components/responses/InternalError' - /node/network/listening: + /network/listen_port: get: tags: - Phase0 - summary: "Identify if the beacon node is listening for networking connections, and on what address." - description: "Requests that the beacon node identify whether it is listening for incoming networking connections, and if so, what network address(es) are being used." + summary: "Get the TCP port number for the libp2p listener." + description: "Libp2p is configured to listen to a particular TCP port upon startup of the beacon node. This endpoint returns the port number that the beacon node is listening on. Please note, this is for the libp2p communications, not for discovery." responses: 200: description: Request successful content: application/json: schema: - type: object - properties: - listening: - type: boolean - nullable: false - description: "True if the node is listening for incoming network connections. False if networking has been disabled or if the node has been configured to only connect with a static set of peers." - listen_address: - $ref: '#/components/schemas/multiaddr' + type: integer + format: uint16 + example: 9000 + 500: + $ref: '#/components/responses/InternalError' - /node/network/stats: + /network/listen_addresses: get: tags: - - Future + - Phase0 + summary: "Identify the port and addresses listened to by the beacon node." + description: "Libp2p is configured to listen to a particular address, on a particular port. This address is represented the [`multiaddr`](https://multiformats.io/multiaddr/) format, and this endpoint requests the beacon node to list all listening addresses in this format." + responses: + 200: + description: Request successful + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/multiaddr' + 500: + $ref: '#/components/responses/InternalError' + + /network/stats: + get: + tags: + - RFC summary: "Get some simple network statistics from the node." description: "Request that the beacon node provide some historical summary information about its networking interface." #TODO: Do we actually collect these stats? Should we? @@ -215,10 +208,10 @@ paths: description: "The total number of unique peers (by multiaddr) that have been discovered since the beacon node instance was started." #TODO: This might be too difficult to collect - /node/network/block_discovery: + /network/block_discovery: get: tags: - - Future + - RFC summary: "Identify the time at which particular blocks were first seen." description: "Request the node to provide the time at which particular blocks were first seen on the network." parameters: @@ -252,179 +245,132 @@ paths: format: uint64 description: "UNIX time in milliseconds that the block was first discovered, either from a network peer or the validator client." - - - #TODO: Add the endpoints that enable a validator to join, exit, withdraw, etc. - /validator/duties: + /beacon/head: get: tags: - Phase0 - summary: "Get validator duties for the requested validators." - description: "Requests the beacon node to provide a set of _duties_, which are actions that should be performed by validators, for a particular epoch. Duties should only need to be checked once per epoch, however a chain reorganization (of > MIN_SEED_LOOKAHEAD epochs) could occur, resulting in a change of duties. For full safety, this API call should be polled at every slot to ensure that chain reorganizations are recognized, and to ensure that the beacon node is properly synchronized. If no epoch parameter is provided, then the current epoch is assumed." + summary: "Detail the current perspective of the beacon node." + description: "Request the beacon node to identify the most up-to-date information about the beacon chain from its perspective. This includes the latest block, which slots have been finalized, etc." + responses: + 200: + description: Success response + content: + application/json: + schema: + type: object + description: "The latest information about the head of the beacon chain." + properties: + slot: + type: integer + format: uint64 + description: "The slot of the head block." + block_root: + type: string + format: bytes + pattern: "^0x[a-fA-F0-9]{64}$" + description: "The merkle tree root of the canonical head block in the beacon node." + state_root: + type: string + format: bytes + pattern: "^0x[a-fA-F0-9]{64}$" + description: "The merkle tree root of the current beacon state." + finalized_slot: + type: integer + format: uint64 + description: "The slot number of the most recent finalized slot." + finalized_block_root: + type: string + format: bytes + pattern: "^0x[a-fA-F0-9]{64}$" + description: "The block root for the most recent finalized block." + justified_slot: + type: integer + format: uint64 + description: "The slot number of the most recent justified slot." + justified_block_root: + type: string + format: bytes + pattern: "^0x[a-fA-F0-9]{64}$" + description: "The block root of the most recent justified block." + previous_justified_slot: + type: integer + format: uint64 + description: "The slot number of the second most recent justified slot." + previous_justified_block_root: + type: string + format: bytes + pattern: "^0x[a-fA-F0-9]{64}$" + description: "The block root of the second most recent justified block." + 500: + $ref: '#/components/responses/InternalError' + + + /beacon/block: + get: + tags: + - Phase0 + summary: 'Retrieve blocks by root or slot.' + description: "Request that the beacon node return beacon chain blocks that match the provided criteria (a block root or beacon chain slot). Only one of the parameters can be be provided at a time." parameters: - - name: validator_pubkeys - in: query - required: true - description: "An array of hex-encoded BLS public keys" - schema: - type: array - items: - $ref: '#/components/schemas/pubkey' - minItems: 1 - - name: epoch - in: query - required: false - schema: + - name: root + description: "Filter by block root." + in: query + required: false + schema: + type: string + format: bytes + pattern: "^0x[a-fA-F0-9]{64}$" + - name: slot + description: "Filter blocks by slot number. Only one block which has been finalized, or is believed to be the canonical block for that slot, is returned." + in: query + required: false + schema: type: integer format: uint64 responses: 200: - description: Success response + description: Success response. content: application/json: schema: type: array items: - $ref: '#/components/schemas/ValidatorDuty' + $ref: '#/components/schemas/BeaconBlock' 400: $ref: '#/components/responses/InvalidRequest' - 406: - description: "Duties cannot be provided for the requested epoch." 500: $ref: '#/components/responses/InternalError' - 503: - $ref: '#/components/responses/CurrentlySyncing' - /validator/block: + /beacon/block_root: get: tags: - Phase0 - summary: "Produce a new block, without signature." - description: "Requests a beacon node to produce a valid block, which can then be signed by a validator." + summary: "Retrieve the canonical block root, given a particular slot." + description: "Request that the beacon node return the root of the canonical beacon chain block, which matches the provided slot number." parameters: - - name: slot - in: query - required: true - description: "The slot for which the block should be proposed." - schema: - type: integer - format: uint64 - - name: randao_reveal - in: query - required: true - description: "The validator's randao reveal value." - schema: - type: string - format: byte - responses: - 200: - description: Success response - content: - application/json: - schema: - $ref: '#/components/schemas/BeaconBlock' - 400: - $ref: '#/components/responses/InvalidRequest' - 500: - $ref: '#/components/responses/InternalError' - 503: - $ref: '#/components/responses/CurrentlySyncing' - post: - tags: - - Phase0 - summary: "Publish a signed block." - description: "Instructs the beacon node to broadcast a newly signed beacon block to the beacon network, to be included in the beacon chain. The beacon node is not required to validate the signed `BeaconBlock`, and a successful response (20X) only indicates that the broadcast has been successful. The beacon node is expected to integrate the new block into its state, and therefore validate the block internally, however blocks which fail the validation are still broadcast but a different status code is returned (202)" - parameters: - - name: beacon_block - in: query - required: true - description: "The `BeaconBlock` object, as sent from the beacon node originally, but now with the signature field completed." - schema: - $ref: '#/components/schemas/BeaconBlock' - responses: - 200: - description: "The block was validated successfully and has been broadcast. It has also been integrated into the beacon node's database." - 202: - description: "The block failed validation, but was successfully broadcast anyway. It was not integrated into the beacon node's database." - 400: - $ref: '#/components/responses/InvalidRequest' - 500: - $ref: '#/components/responses/InternalError' - 503: - $ref: '#/components/responses/CurrentlySyncing' - - /validator/attestation: - get: - tags: - - Phase0 - summary: "Produce an attestation, without signature." - description: "Requests that the beacon node produce an IndexedAttestation, with a blank signature field, which the validator will then sign." - parameters: - - name: validator_pubkey - in: query - required: true - description: "Uniquely identifying which validator this attestation is to be produced for." - schema: - $ref: '#/components/schemas/pubkey' - - name: poc_bit - in: query - required: true - description: "The proof-of-custody bit that is to be reported by the requesting validator. This bit will be inserted into the appropriate location in the returned `IndexedAttestation`." - schema: + - name: slot + description: "Filter blocks by slot number. Only one block which has been finalized, or is believed to be the canonical block for that slot, is returned." + in: query + required: true + schema: type: integer - format: uint32 - minimum: 0 - maximum: 1 - - name: slot - in: query - required: true - description: "The slot for which the attestation should be proposed." - schema: - type: integer - - name: shard - in: query - required: true - description: "The shard number for which the attestation is to be proposed." - schema: - type: integer + format: uint64 responses: 200: - description: Success response + description: Success response. content: application/json: schema: - $ref: '#/components/schemas/IndexedAttestation' + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "The 0x prefixed block root." 400: $ref: '#/components/responses/InvalidRequest' 500: $ref: '#/components/responses/InternalError' - 503: - $ref: '#/components/responses/CurrentlySyncing' - post: - tags: - - Phase0 - summary: "Publish a signed attestation." - description: "Instructs the beacon node to broadcast a newly signed IndexedAttestation object to the intended shard subnet. The beacon node is not required to validate the signed IndexedAttestation, and a successful response (20X) only indicates that the broadcast has been successful. The beacon node is expected to integrate the new attestation into its state, and therefore validate the attestation internally, however attestations which fail the validation are still broadcast but a different status code is returned (202)" - parameters: - - name: attestation - in: query - required: true - description: "An `IndexedAttestation` structure, as originally provided by the beacon node, but now with the signature field completed." - schema: - $ref: '#/components/schemas/IndexedAttestation' - responses: - 200: - description: "The attestation was validated successfully and has been broadcast. It has also been integrated into the beacon node's database." - 202: - description: "The attestation failed validation, but was successfully broadcast anyway. It was not integrated into the beacon node's database." - 400: - $ref: '#/components/responses/InvalidRequest' - 500: - $ref: '#/components/responses/InternalError' - 503: - $ref: '#/components/responses/CurrentlySyncing' - /chain/beacon/blocks: + /beacon/blocks: get: tags: - Phase0 @@ -468,59 +414,25 @@ paths: $ref: '#/components/responses/InvalidRequest' #TODO: Make this request error more specific if one of the parameters is not provided correctly. - /chain/beacon/chainhead: + + /beacon/fork: get: tags: - Phase0 - summary: "Detail the current perspective of the beacon node." - description: "Request the beacon node to identify the most up-to-date information about the beacon chain from its perspective. This includes the latest block, which slots have been finalized, etc." + summary: 'Retrieve the current Fork information.' + description: 'Request the beacon node identify the fork it is currently on, from the beacon state.' responses: 200: - description: Success response + description: Success response. content: application/json: schema: - type: object - description: "The latest information about the head of the beacon chain." - properties: - block_root: - type: string - format: bytes - pattern: "^0x[a-fA-F0-9]{64}$" - description: "The merkle tree root of the canonical head block in the beacon node." - block_slot: - type: integer - format: uint64 - description: "The slot of the head block." - finalized_slot: - type: integer - format: uint64 - description: "The slot number of the most recent finalized slot." - finalized_block_root: - type: string - format: bytes - pattern: "^0x[a-fA-F0-9]{64}$" - description: "The block root for the most recent finalized block." - justified_slot: - type: integer - format: uint64 - description: "The slot number of the most recent justified slot." - justified_block_root: - type: string - format: bytes - pattern: "^0x[a-fA-F0-9]{64}$" - description: "The block root of the most recent justified block." - previous_justified_slot: - type: integer - format: uint64 - description: "The slot number of the second most recent justified slot." - previous_justified_block_root: - type: integer - format: bytes - pattern: "^0x[a-fA-F0-9]{64}$" - description: "The block root of the second most recent justified block." + $ref: '#/components/schemas/Fork' + 500: + $ref: '#/components/responses/InternalError' - /chain/beacon/attestations: + + /beacon/attestations: get: tags: - Phase0 @@ -564,7 +476,7 @@ paths: $ref: '#/components/responses/InvalidRequest' #TODO: Make this request error more specific if one of the parameters is not provided correctly. - /chain/beacon/attestations/pending: + /beacon/attestations/pending: get: tags: - Phase0 @@ -583,7 +495,7 @@ paths: $ref: '#/components/responses/InvalidRequest' #TODO: Make this request error more specific if one of the parameters is not provided correctly. - /chain/beacon/validators: + /beacon/validators: get: tags: - Phase0 @@ -612,12 +524,12 @@ paths: validators: type: array items: - $ref: '#/components/schemas/ValidatorInfo' + $ref: '#/components/schemas/Validator' - /chain/beacon/validators/activesetchanges: + /beacon/validators/activesetchanges: get: tags: - - Phase0 + - RFC summary: "Retrieve the changes in active validator set." description: "Request that the beacon node describe the changes that occurred at the specified epoch, as compared with the prior epoch." parameters: @@ -656,10 +568,10 @@ paths: items: $ref: '#/components/schemas/pubkey' - /chain/beacon/validators/assignments: + /beacon/validators/assignments: get: tags: - - Phase0 + - RFC summary: "Retrieve the assigned responsibilities for validators in a particular epoch." description: "Request that the beacon node list the duties which have been assigned to the active validator set in a particular epoch." parameters: @@ -688,7 +600,7 @@ paths: $ref: '#/components/schemas/ValidatorDuty' #TODO: This does not include the crosslink committee value, which must be included for Phase1? - /chain/beacon/validators/indices: + /beacon/validators/indices: get: tags: - Phase0 @@ -714,7 +626,7 @@ paths: items: $ref: '#/components/schemas/ValidatorIndexMapping' - /chain/beacon/validators/pubkeys: + /beacon/validators/pubkeys: get: tags: - Phase0 @@ -742,10 +654,10 @@ paths: items: $ref: '#/components/schemas/ValidatorIndexMapping' - /chain/beacon/validators/balances: + /beacon/validators/balances: get: tags: - - Phase0 + - RFC summary: "Retrieve the balances of validators at a specified epoch." description: "Retrieve the balances of validators at a specified epoch (or the current epoch if none specified). The list of balances can be filtered by providing a list of validator public keys or indices." parameters: @@ -803,10 +715,10 @@ paths: format: uint64 description: "The balance of the validator at the specified epoch, expressed in Gwei" - /chain/beacon/validators/participation: + /beacon/validators/participation: get: tags: - - Phase0 + - RFC summary: "Retrieve aggregate information about validator participation in an epoch." description: "Retrieve some aggregate information about the participation of validators in a specified epoch (or the current epoch if none specified)." parameters: @@ -848,10 +760,10 @@ paths: format: uint64 description: "The total amount of ether, expressed in Gwei, that is eligible for voting in the specified epoch." - /chain/beacon/validators/queue: + /beacon/validators/queue: get: tags: - - Phase0 + - RFC summary: "Retrieve information about the validator queue at the specified epoch." description: "Retrieve information about the queue of validators for the specified epoch (or the current epoch if none specified)." parameters: @@ -889,6 +801,407 @@ paths: items: $ref: '#/components/schemas/pubkey' + #TODO: Add the endpoints that enable a validator to join, exit, withdraw, etc. + /beacon/validator/duties: + get: + tags: + - Phase0 + summary: "Get validator duties for the requested validators." + description: "Requests the beacon node to provide a set of _duties_, which are actions that should be performed by validators, for a particular epoch. Duties should only need to be checked once per epoch, however a chain reorganization (of > MIN_SEED_LOOKAHEAD epochs) could occur, resulting in a change of duties. For full safety, this API call should be polled at every slot to ensure that chain reorganizations are recognized, and to ensure that the beacon node is properly synchronized. If no epoch parameter is provided, then the current epoch is assumed." + parameters: + - name: validator_pubkeys + in: query + required: true + description: "An array of hex-encoded BLS public keys" + schema: + type: array + items: + $ref: '#/components/schemas/pubkey' + minItems: 1 + - name: epoch + in: query + required: false + schema: + type: integer + format: uint64 + responses: + 200: + description: Success response + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ValidatorDuty' + 400: + $ref: '#/components/responses/InvalidRequest' + 500: + $ref: '#/components/responses/InternalError' + 503: + $ref: '#/components/responses/CurrentlySyncing' + + /beacon/validator/block: + get: + tags: + - Phase0 + summary: "Produce a new block, without signature." + description: "Requests a beacon node to produce a valid block, which can then be signed by a validator." + parameters: + - name: slot + in: query + required: true + description: "The slot for which the block should be proposed." + schema: + type: integer + format: uint64 + - name: randao_reveal + in: query + required: true + description: "The validator's randao reveal value." + schema: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{192}$" + description: "A valid BLS signature." + responses: + 200: + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/BeaconBlock' + 400: + $ref: '#/components/responses/InvalidRequest' + 500: + $ref: '#/components/responses/InternalError' + 503: + $ref: '#/components/responses/CurrentlySyncing' + post: + tags: + - Phase0 + summary: "Publish a signed block." + description: "Instructs the beacon node to broadcast a newly signed beacon block to the beacon network, to be included in the beacon chain. The beacon node is not required to validate the signed `BeaconBlock`, and a successful response (20X) only indicates that the broadcast has been successful. The beacon node is expected to integrate the new block into its state, and therefore validate the block internally, however blocks which fail the validation are still broadcast but a different status code is returned (202)" + requestBody: + description: "The `BeaconBlock` object, as sent from the beacon node originally, but now with the signature field completed. Must be sent in JSON format in the body of the request." + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BeaconBlock' + responses: + 200: + description: "The block was validated successfully and has been broadcast. It has also been integrated into the beacon node's database." + 202: + description: "The block failed validation, but was successfully broadcast anyway. It was not integrated into the beacon node's database." + 400: + $ref: '#/components/responses/InvalidRequest' + 500: + $ref: '#/components/responses/InternalError' + 503: + $ref: '#/components/responses/CurrentlySyncing' + + /beacon/validator/attestation: + get: + tags: + - Phase0 + summary: "Produce an attestation, without signature." + description: "Requests that the beacon node produce an Attestation, with a blank signature field, which the validator will then sign." + parameters: + - name: validator_pubkey + in: query + required: true + description: "Uniquely identifying which validator this attestation is to be produced for." + schema: + $ref: '#/components/schemas/pubkey' + - name: poc_bit + in: query + required: true + description: "The proof-of-custody bit that is to be reported by the requesting validator. This bit will be inserted into the appropriate location in the returned `Attestation`." + schema: + type: integer + format: uint32 + minimum: 0 + maximum: 1 + - name: slot + in: query + required: true + description: "The slot for which the attestation should be proposed." + schema: + type: integer + - name: shard + in: query + required: true + description: "The shard number for which the attestation is to be proposed." + schema: + type: integer + responses: + 200: + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/Attestation' + 400: + $ref: '#/components/responses/InvalidRequest' + 500: + $ref: '#/components/responses/InternalError' + 503: + $ref: '#/components/responses/CurrentlySyncing' + post: + tags: + - Phase0 + summary: "Publish a signed attestation." + description: "Instructs the beacon node to broadcast a newly signed Attestation object to the intended shard subnet. The beacon node is not required to validate the signed Attestation, and a successful response (20X) only indicates that the broadcast has been successful. The beacon node is expected to integrate the new attestation into its state, and therefore validate the attestation internally, however attestations which fail the validation are still broadcast but a different status code is returned (202)" + requestBody: + description: "An `Attestation` structure, as originally provided by the beacon node, but now with the signature field completed. Must be sent in JSON format in the body of the request." + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Attestation' + responses: + 200: + description: "The attestation was validated successfully and has been broadcast. It has also been integrated into the beacon node's database." + 202: + description: "The attestation failed validation, but was successfully broadcast anyway. It was not integrated into the beacon node's database." + 400: + $ref: '#/components/responses/InvalidRequest' + 500: + $ref: '#/components/responses/InternalError' + 503: + $ref: '#/components/responses/CurrentlySyncing' + + /beacon/state: + get: + tags: + - Phase0 + summary: "Get the full beacon state, at a particular slot or block root." + description: "Requests the beacon node to provide the full beacon state object, and the state root, given a particular slot number or block root. If no parameters are provided, the latest slot of the beacon node (the 'head' slot) is used." + parameters: + - name: root + description: "The block root at which the state should be provided." + in: query + required: false + schema: + type: string + format: bytes + pattern: "^0x[a-fA-F0-9]{64}$" + - name: slot + description: "The slot number at which the state should be provided." + in: query + required: false + schema: + type: integer + format: uint64 + responses: + 200: + description: Success response + content: + application/json: + schema: + type: object + properties: + root: + type: string + format: bytes + pattern: "^0x[a-fA-F0-9]{64}$" + beacon_state: + $ref: '#/components/schemas/BeaconState' + 400: + $ref: '#/components/responses/InvalidRequest' + 500: + $ref: '#/components/responses/InternalError' + + /beacon/state_root: + get: + tags: + - Phase0 + summary: "Get the beacon state root, at a particular slot." + description: "Requests the beacon node to provide the root of the beacon state object, given a particular slot number." + parameters: + - name: slot + description: "The slot number at which the state should be provided." + in: query + required: true + schema: + type: integer + format: uint64 + responses: + 200: + description: Success response + content: + application/json: + schema: + type: object + properties: + root: + type: string + format: bytes + pattern: "^0x[a-fA-F0-9]{64}$" + description: "The state root" + 400: + $ref: '#/components/responses/InvalidRequest' + 500: + $ref: '#/components/responses/InternalError' + + /beacon/state/current_finalized_checkpoint: + get: + tags: + - Phase0 + summary: "Get the current finalized checkpoint." + #TODO: is this description correct? + description: "Requests the beacon node to provide the checkpoint for the current finalized epoch." + responses: + 200: + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/Checkpoint' + 500: + $ref: '#/components/responses/InternalError' + + /beacon/state/genesis: + get: + tags: + - Phase0 + summary: "Get the full beacon state, as it was at genesis." + description: "Requests the beacon node to provide the full beacon state object and the state root, as it was for the genesis block." + responses: + 200: + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/BeaconState' + application/yaml: + schema: + $ref: '#/components/schemas/BeaconState' + 400: + $ref: '#/components/responses/InvalidRequest' + 500: + $ref: '#/components/responses/InternalError' + + /spec: + get: + tags: + - Phase0 + summary: "Get the current ChainSpec configuration." + description: "Requests the beacon node to provide the configuration that it has used to start the beacon chain." + responses: + 200: + description: Success response + content: + application/json: + schema: + $ref: '#/components/schemas/ChainSpec' + 500: + $ref: '#/components/responses/InternalError' + + /spec/slots_per_epoch: + get: + tags: + - Phase0 + summary: "Get the configured number of slots per epoch." + description: "The number of slots in each epoch is part of the Eth2.0 spec. This function simply returns an integer representing this value." + responses: + 200: + description: Success response + content: + application/json: + schema: + type: integer + format: uint64 + example: 64 + 500: + $ref: '#/components/responses/InternalError' + + /spec/deposit_contract: + get: + tags: + - Phase0 + summary: "Get the address of the Ethereum 1 deposit contract." + description: "Requests the address of the deposit contract on the Ethereum 1 chain, which was used to start the current beacon chain." + responses: + 200: + description: Request successful + content: + application/json: + schema: + $ref: '#/components/schemas/ethereum_address' + 500: + $ref: '#/components/responses/InternalError' + + /spec/eth2_config: + get: + tags: + - Phase0 + summary: "Gets the Eth2.0 spec, including the identifying string." + description: "" + responses: + 200: + description: Success response + content: + application/json: + schema: + type: object + properties: + spec_constants: + type: string + example: "mainnet" + spec: + $ref: '#/components/schemas/ChainSpec' + 500: + $ref: '#/components/responses/InternalError' + + /metrics: + get: + tags: + - Phase0 + summary: "Get Promethius metrics for the node" + description: "Fetches a range of metrics for measuring nodes health. It is intended for this endpoint to be consumed by Promethius." + responses: + 200: + description: Request successful + content: + text/plain: + example: "# HELP beacon_head_state_active_validators_total Count of active validators at the head of the chain + # TYPE beacon_head_state_active_validators_total gauge + beacon_head_state_active_validators_total 16 + # HELP beacon_head_state_current_justified_epoch Current justified epoch at the head of the chain + # TYPE beacon_head_state_current_justified_epoch gauge + beacon_head_state_current_justified_epoch 0 + # HELP beacon_head_state_current_justified_root Current justified root at the head of the chain + # TYPE beacon_head_state_current_justified_root gauge + beacon_head_state_current_justified_root 0 + # HELP beacon_head_state_eth1_deposit_index Eth1 deposit index at the head of the chain + # TYPE beacon_head_state_eth1_deposit_index gauge + beacon_head_state_eth1_deposit_index 16 + # HELP beacon_head_state_finalized_epoch Finalized epoch at the head of the chain + # TYPE beacon_head_state_finalized_epoch gauge + beacon_head_state_finalized_epoch 0 + # HELP beacon_head_state_finalized_root Finalized root at the head of the chain + # TYPE beacon_head_state_finalized_root gauge + beacon_head_state_finalized_root 0 + # HELP beacon_head_state_latest_block_slot Latest block slot at the head of the chain + # TYPE beacon_head_state_latest_block_slot gauge + beacon_head_state_latest_block_slot 0 + # HELP beacon_head_state_previous_justified_epoch Previous justified epoch at the head of the chain + # TYPE beacon_head_state_previous_justified_epoch gauge + beacon_head_state_previous_justified_epoch 0 + # HELP beacon_head_state_previous_justified_root Previous justified root at the head of the chain + # TYPE beacon_head_state_previous_justified_root gauge + beacon_head_state_previous_justified_root 0 + # HELP beacon_head_state_root Root of the block at the head of the chain + # TYPE beacon_head_state_root gauge + beacon_head_state_root -7566315470565629000 + # HELP beacon_head_state_shard_total Count of shards in the beacon chain + # TYPE beacon_head_state_shard_total gauge + beacon_head_state_shard_total 8 + # HELP beacon_head_state_slashed_validators_total Count of all slashed validators at the head of the chain + # TYPE beacon_head_state_slashed_validators_total gauge + beacon_head_state_slashed_validators_total 0" + components: schemas: pubkey: @@ -928,6 +1241,34 @@ components: pattern: "^0x[a-fA-F0-9]{64}$" description: "A hex encoded ethereum address." + ENR: + type: string + format: byte + example: "-IW4QHzEZbIB0YN47bVlsUrGbcL9vl21n7xF5gRKjMNkJ4MxfcwiqrsE7Ows8EnzOvC8P4ZyAjfOhr2ffk0bWAxDGq8BgmlwhH8AAAGDdGNwgiMog3VkcIIjKIlzZWNwMjU2azGhAjzKzqo5c33ydUUHrWJ4FWwIXJa2MN9BBsgZkj6mhthp" + pattern: "^[^-A-Za-z0-9+/=]+$" + + Shard: + type: integer + format: uint64 + description: "A shard number." + example: 5 + maximum: 1023 + minimum: 0 + + Checkpoint: + type: object + description: "A checkpoint." + properties: + epoch: + type: integer + format: uint64 + description: "The epoch to which the checkpoint applies." + root: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "A block root, which is being checkpointed." + Peer: type: object properties: @@ -957,7 +1298,7 @@ components: format: uint64 description: "The global ValidatorIndex value." - ValidatorInfo: + Validator: type: object properties: public_key: @@ -967,6 +1308,16 @@ components: format: bytes pattern: "^0x[a-fA-F0-9]{64}$" description: "The 32 byte hash of the public key which the validator uses for withdrawing their rewards." + example: "0x00ec7ef7780c9d151597924036262dd28dc60e1228f4da6fecf9d402cb3f3594" + effective_balance: + type: integer + format: uint64 + description: "The effective balance of the validator, measured in Gwei." + example: 32000000000 + slashed: + type: boolean + description: "Whether the validator has or has not been slashed." + example: false activation_eligiblity_epoch: type: integer format: uint64 @@ -980,18 +1331,13 @@ components: format: uint64 nullable: true description: "Epoch when the validator was exited, or null if the validator has not exited." + example: 18446744073709551615 withdrawable_epoch: type: integer format: uint64 nullable: true description: "Epoch when the validator is eligible to withdraw their funds, or null if the validator has not exited." - slashed: - type: boolean - description: "Whether the validator has or has not been slashed." - effective_balance: - type: integer - format: uint64 - description: "The effective balance of the validator, measured in Gwei." + example: 18446744073709551615 ValidatorDuty: type: object @@ -1029,6 +1375,25 @@ components: format: uint64 description: "Globally, the estimated most recent slot number, or current target slot number." + Eth1Data: + type: object + description: "The [`Eth1Data`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#eth1data) object from the Eth2.0 spec." + properties: + deposit_root: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "Root of the deposit tree." + deposit_count: + type: integer + format: uint64 + description: "Total number of deposits." + block_hash: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "Ethereum 1.x block hash." + BeaconBlock: description: "The [`BeaconBlock`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#beaconblock) object from the Eth2.0 spec." allOf: @@ -1085,24 +1450,7 @@ components: pattern: "^0x[a-fA-F0-9]{192}$" description: "The RanDAO reveal value provided by the validator." eth1_data: - title: Eth1Data - type: object - description: "The [`Eth1Data`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#eth1data) object from the Eth2.0 spec." - properties: - deposit_root: - type: string - format: byte - pattern: "^0x[a-fA-F0-9]{64}$" - description: "Root of the deposit tree." - deposit_count: - type: integer - format: uint64 - description: "Total number of deposits." - block_hash: - type: string - format: byte - pattern: "^0x[a-fA-F0-9]{64}$" - description: "Ethereum 1.x block hash." + $ref: '#/components/schemas/Eth1Data' graffiti: type: string format: byte @@ -1130,9 +1478,9 @@ components: description: "The [`AttesterSlashing`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attesterslashing) object from the Eth2.0 spec." properties: attestation_1: - $ref: '#/components/schemas/IndexedAttestation' + $ref: '#/components/schemas/Attestation' attestation_2: - $ref: '#/components/schemas/IndexedAttestation' + $ref: '#/components/schemas/Attestation' attestations: type: array items: @@ -1236,6 +1584,161 @@ components: pattern: "^0x[a-fA-F0-9]{192}$" description: "Sender signature." + BeaconState: + type: object + description: "The [`BeaconState`](https://github.com/ethereum/eth2.0-specs/blob/dev/specs/core/0_beacon-chain.md#beaconstate) object from the Eth2.0 spec." + properties: + genesis_time: + $ref: '#/components/schemas/genesis_time' + slot: + type: integer + format: uint64 + description: "The latest slot, which the state represents." + fork: + $ref: '#/components/schemas/Fork' + latest_block_header: + $ref: '#/components/schemas/BeaconBlockHeader' + #TODO: Are these descriptions correct? + block_roots: + type: array + description: "The historical block roots." + minLength: 8192 + maxLength: 8192 #The SLOTS_PER_HISTORICAL_ROOT value from the Eth2.0 Spec. + items: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "A block root" + state_roots: + type: array + description: "The historical state roots." + minLength: 8192 + maxLength: 8192 #The SLOTS_PER_HISTORICAL_ROOT value from the Eth2.0 Spec. + items: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "A state root" + historical_roots: + type: array + #TODO: are these historical *state* roots? + description: "The historical state roots." + maxLength: 16777216 #The HISTORICAL_ROOTS_LIMIT value from the Eth2.0 Spec. + items: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "A state root" + eth1_data: + $ref: '#/components/schemas/Eth1Data' + eth1_data_votes: + type: array + description: "The validator votes for the Eth1Data." + maxLength: 1024 #The SLOTS_PER_ETH1_VOTING_PERIOD value from the Eth2.0 spec. + items: + $ref: '#/components/schemas/Eth1Data' + eth1_deposit_index: + type: integer + format: uint64 + #TODO: Clarify this description + description: "The index of the Eth1 deposit." + validators: + type: array + description: "A list of the current validators." + maxLength: 1099511627776 + items: + $ref: '#/components/schemas/Validator' + balances: + type: array + description: "An array of the validator balances." + maxLength: 1099511627776 + items: + type: integer + format: uint64 + description: "The validator balance in GWei." + start_shard: + $ref: '#/components/schemas/Shard' + randao_mixes: + type: array + description: "The hashes for the randao mix." + minLength: 65536 + maxLength: 65536 #The EPOCHS_PER_HISTORICAL_VECTOR value from the Eth2.0 spec. + items: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "A randao mix hash." + active_index_roots: + type: array + description: "Active index digests for light clients." + minLength: 65536 + maxLength: 65536 #The EPOCHS_PER_HISTORICAL_VECTOR value from the Eth2.0 spec. + items: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "Active index digest" + compact_committees_roots: + type: array + description: "Committee digests for light clients." + minLength: 65536 + maxLength: 65536 #The EPOCHS_PER_HISTORICAL_VECTOR value from the Eth2.0 spec. + items: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "Committee digest." + slashings: + type: array + description: "Per-epoch sums of slashed effective balances." + minLength: 8192 + maxLength: 8192 #The EPOCHS_PER_SLASHINGS_VECTOR value from the Eth2.0 spec. + items: + type: integer + format: uint64 + description: "Sum of slashed balance for an epoch." + previous_epoch_attestations: + type: array + description: "A list of attestations in the previous epoch." + maxLength: 8192 # MAX_ATTESTATIONS * SLOTS_PER_EPOCH from the Eth2.0 spec. + items: + $ref: '#/components/schemas/PendingAttestation' + current_epoch_attestations: + type: array + description: "A list of attestations in the current epoch." + maxLength: 8192 # MAX_ATTESTATIONS * SLOTS_PER_EPOCH from the Eth2.0 spec. + items: + $ref: '#/components/schemas/PendingAttestation' + previous_crosslinks: + type: array + description: "The shard crosslinks from the previous epoch." + minLength: 1024 + maxLength: 1024 #The SHARD_COUNT value from the Eth2.0 spec + items: + $ref: '#/components/schemas/Crosslink' + current_crosslinks: + type: array + description: "The shard crosslinks for the current epoch." + minLength: 1024 + maxLength: 1024 #The SHARD_COUNT value from the Eth2.0 spec + items: + $ref: '#/components/schemas/Crosslink' + justification_bits: + type: array + description: "Bit set for every recent justified epoch." + minLength: 4 + maxLength: 4 #The JUSTIFICATION_BITS_LENGTH from the Eth2.0 spec. + items: + type: boolean + #TODO: Check this description + description: "Whethere the recent epochs have been finalized." + previous_justified_checkpoint: + $ref: '#/components/schemas/Checkpoint' + current_justified_checkpoint: + $ref: '#/components/schemas/Checkpoint' + finalized_checkpoint: + $ref: '#/components/schemas/Checkpoint' + Fork: type: object description: "The [`Fork`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#Fork) object from the Eth2.0 spec." @@ -1302,6 +1805,35 @@ components: data: $ref: '#/components/schemas/AttestationData' + PendingAttestation: + type: object + description: "The [`PendingAttestation`](https://github.com/ethereum/eth2.0-specs/blob/v0.8.3/specs/core/0_beacon-chain.md#pendingattestation) object from the Eth2.0 spec." + properties: + aggregation_bits: + type: array + description: "The bits representing aggregation of validator signatures and attestations." + maxLength: 4096 #The MAX_VALIDATORS_PER_COMMITTEE value from the Eth2.0 spec. + items: + type: boolean + description: "Whether the validator has been aggregated or not" + data: + $ref: '#/components/schemas/AttestationData' + inclusion_delay: + type: integer + format: uint64 + description: "The Slot at which it should be included." + proposer_index: + type: integer + format: uint64 + #TODO: This is the block proposer index, not the attestaion right? + description: "The ValidatorIndex of the block proposer" + + + + + + + AttestationData: type: object description: "The [`AttestationData`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attestationdata) object from the Eth2.0 spec." @@ -1330,32 +1862,185 @@ components: pattern: "^0x[a-fA-F0-9]{64}$" description: "Target root from FFG vote." crosslink: - title: CrossLink - type: object - description: "The [`Crosslink`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#crosslink) object from the Eth2.0 spec, contains data from epochs [`start_epoch`, `end_epoch`)." - properties: - shard: - type: integer - format: uint64 - description: "The shard number." - start_epoch: - type: integer - format: uint64 - description: "The first epoch which the crosslinking data references." - end_epoch: - type: integer - format: uint64 - description: "The 'end' epoch referred to by the crosslinking data; no data in this Crosslink should refer to the `end_epoch` since it is not included in the crosslinking data interval." - parent_root: - type: string - format: byte - pattern: "^0x[a-fA-F0-9]{64}$" - description: "Root of the previous crosslink." - data_root: - type: string - format: byte - pattern: "^0x[a-fA-F0-9]{64}$" - description: "Root of the crosslinked shard data since the previous crosslink." + $ref: '#/components/schemas/Crosslink' + + + Crosslink: + type: object + description: "The [`Crosslink`](https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#crosslink) object from the Eth2.0 spec, contains data from epochs [`start_epoch`, `end_epoch`)." + properties: + shard: + type: integer + format: uint64 + description: "The shard number." + start_epoch: + type: integer + format: uint64 + description: "The first epoch which the crosslinking data references." + end_epoch: + type: integer + format: uint64 + description: "The 'end' epoch referred to by the crosslinking data; no data in this Crosslink should refer to the `end_epoch` since it is not included in the crosslinking data interval." + parent_root: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "Root of the previous crosslink." + data_root: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{64}$" + description: "Root of the crosslinked shard data since the previous crosslink." + + ChainSpec: + type: object + description: "Stores all of the values which specify a particular chain. The `ChainSpec` object in Lighthouse" + properties: + base_rewards_per_epoch: + type: integer + format: uint64 + example: 5 + deposit_contract_tree_depth: + type: integer + format: uint64 + example: 32 + seconds_per_day: + type: integer + format: uint64 + example: 86400 + target_committee_size: + type: integer + format: uint64 + example: 128 + min_per_epoch_churn_limit: + type: integer + format: uint64 + example: 4 + churn_limit_quotient: + type: integer + format: uint64 + example: 65536 + shuffle_round_count: + type: integer + format: uint8 + example: 90 + min_genesis_active_validator_count: + type: integer + format: uint64 + example: 65536 + min_genesis_time: + type: integer + format: uint64 + example: 1578009600 + min_deposit_amount: + type: integer + format: uint64 + example: 1000000000 + max_effective_balance: + type: integer + format: uint64 + example: 32000000000 + ejection_balance: + type: integer + format: uint64 + example: 16000000000 + effective_balance_increment: + type: integer + format: uint64 + example: 1000000000 + genesis_slot: + type: integer + format: uint64 + example: 0 + bls_withdrawal_prefix_byte: + type: string + format: byte + pattern: "^0x[a-fA-F0-9]{2}$" + example: "0x00" + milliseconds_per_slot: + type: integer + format: uint64 + example: 6000 + min_attestation_inclusion_delay: + type: integer + format: uint64 + example: 1 + min_seed_lookahead: + type: integer + format: uint64 + example: 1 + activation_exit_delay: + type: integer + format: uint64 + example: 4 + min_validator_withdrawability_delay: + type: integer + format: uint64 + example: 256 + persistent_committee_period: + type: integer + format: uint64 + example: 2048 + max_epochs_per_crosslink: + type: integer + format: uint64 + example: 64 + min_epochs_to_inactivity_penalty: + type: integer + format: uint64 + example: 4 + base_reward_factor: + type: integer + format: uint64 + example: 64 + whistleblower_reward_quotient: + type: integer + format: uint64 + example: 512 + proposer_reward_quotient: + type: integer + format: uint64 + example: 8 + inactivity_penalty_quotient: + type: integer + format: uint64 + example: 33554432 + min_slashing_penalty_quotient: + type: integer + format: uint64 + example: 32 + domain_beacon_proposer: + type: integer + format: uint32 + example: 0 + domain_randao: + type: integer + format: uint32 + example: 1 + domain_attestation: + type: integer + format: uint32 + example: 2 + domain_deposit: + type: integer + format: uint32 + example: 3 + domain_voluntary_exit: + type: integer + format: uint32 + example: 4 + domain_transfer: + type: integer + format: uint32 + example: 5 + boot_nodes: + type: array + items: + $ref: '#/components/schemas/ENR' + network_id: + type: integer + format: uint8 + example: 2 responses: diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index dae07d76c..f6961cba8 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -26,6 +26,7 @@ slot_clock = { path = "../eth2/utils/slot_clock" } types = { path = "../eth2/types" } serde = "1.0" serde_derive = "1.0" +serde_json = "^1.0" slog = { version = "^2.2.3" , features = ["max_level_trace", "release_max_level_trace"] } slog-async = "^2.3.0" slog-json = "^2.3" diff --git a/validator_client/src/block_producer/mod.rs b/validator_client/src/block_producer/mod.rs index 03d9f5946..bb9c5741d 100644 --- a/validator_client/src/block_producer/mod.rs +++ b/validator_client/src/block_producer/mod.rs @@ -6,7 +6,8 @@ pub use self::beacon_node_block::{BeaconNodeError, PublishOutcome}; pub use self::grpc::BeaconBlockGrpcClient; use crate::signer::Signer; use core::marker::PhantomData; -use slog::{error, info, warn}; +use serde_json; +use slog::{error, info, trace, warn}; use std::sync::Arc; use tree_hash::{SignedRoot, TreeHash}; use types::{BeaconBlock, ChainSpec, Domain, EthSpec, Fork, Slot}; @@ -53,30 +54,32 @@ pub struct BlockProducer<'a, B: BeaconNodeBlock, S: Signer, E: EthSpec> { pub slots_per_epoch: u64, /// Mere vessel for E. pub _phantom: PhantomData, + /// The logger, for logging + pub log: slog::Logger, } impl<'a, B: BeaconNodeBlock, S: Signer, E: EthSpec> BlockProducer<'a, B, S, E> { /// Handle outputs and results from block production. - pub fn handle_produce_block(&mut self, log: slog::Logger) { + pub fn handle_produce_block(&mut self) { match self.produce_block() { Ok(ValidatorEvent::BlockProduced(slot)) => info!( - log, + self.log, "Block produced"; "validator" => format!("{}", self.signer), "slot" => slot, ), - Err(e) => error!(log, "Block production error"; "Error" => format!("{:?}", e)), + Err(e) => error!(self.log, "Block production error"; "Error" => format!("{:?}", e)), Ok(ValidatorEvent::SignerRejection(_slot)) => { - error!(log, "Block production error"; "Error" => "Signer Could not sign the block".to_string()) + error!(self.log, "Block production error"; "Error" => "Signer Could not sign the block".to_string()) } Ok(ValidatorEvent::SlashableBlockNotProduced(_slot)) => { - error!(log, "Block production error"; "Error" => "Rejected the block as it could have been slashed".to_string()) + error!(self.log, "Block production error"; "Error" => "Rejected the block as it could have been slashed".to_string()) } Ok(ValidatorEvent::BeaconNodeUnableToProduceBlock(_slot)) => { - error!(log, "Block production error"; "Error" => "Beacon node was unable to produce a block".to_string()) + error!(self.log, "Block production error"; "Error" => "Beacon node was unable to produce a block".to_string()) } Ok(v) => { - warn!(log, "Unknown result for block production"; "Error" => format!("{:?}",v)) + warn!(self.log, "Unknown result for block production"; "Error" => format!("{:?}",v)) } } } @@ -93,14 +96,21 @@ impl<'a, B: BeaconNodeBlock, S: Signer, E: EthSpec> BlockProducer<'a, B, S, E> { /// slashing. pub fn produce_block(&mut self) -> Result { let epoch = self.slot.epoch(self.slots_per_epoch); + trace!(self.log, "Producing block"; "epoch" => epoch); let message = epoch.tree_hash_root(); let randao_reveal = match self.signer.sign_message( &message, self.spec.get_domain(epoch, Domain::Randao, &self.fork), ) { - None => return Ok(ValidatorEvent::SignerRejection(self.slot)), - Some(signature) => signature, + None => { + warn!(self.log, "Signing rejected"; "message" => format!("{:?}", message)); + return Ok(ValidatorEvent::SignerRejection(self.slot)); + } + Some(signature) => { + info!(self.log, "Signed tree_hash_root for randao_reveal"; "message" => format!("{:?}", message), "signature" => serde_json::to_string(&signature).expect("We should always be able to serialize a signature as JSON.")); + signature + } }; if let Some(block) = self diff --git a/validator_client/src/main.rs b/validator_client/src/main.rs index 58914a9a8..30ed95661 100644 --- a/validator_client/src/main.rs +++ b/validator_client/src/main.rs @@ -55,7 +55,6 @@ fn main() { ) .arg( Arg::with_name("spec") - .short("s") .long("spec") .value_name("TITLE") .help("Specifies the default eth2 spec type.") @@ -143,6 +142,15 @@ fn main() { .help("Path to a YAML file.")) ) ) + .subcommand(SubCommand::with_name("sign_block") + .about("Connects to the beacon server, requests a new block (after providing reveal),\ + and prints the signed block to standard out") + .arg(Arg::with_name("validator") + .value_name("VALIDATOR") + .required(true) + .help("The pubkey of the validator that should sign the block.") + ) + ) .get_matches(); let drain = match matches.value_of("debug-level") { diff --git a/validator_client/src/service.rs b/validator_client/src/service.rs index fd8de71ca..0c42211e2 100644 --- a/validator_client/src/service.rs +++ b/validator_client/src/service.rs @@ -392,8 +392,9 @@ impl Service, + log, }; - block_producer.handle_produce_block(log); + block_producer.handle_produce_block(); }); } if work_type.attestation_duty.is_some() {