diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 5d7def3002..6df72c9981 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -175,7 +175,7 @@ impl BeaconChain { ) -> Result>, Error> { let bodies: Result, _> = roots .iter() - .map(|root| match self.get_block(root)? { + .map(|root| match self.block_at_root(*root)? { Some(block) => Ok(block.body), None => Err(Error::DBInconsistent(format!("Missing block: {}", root))), }) @@ -190,7 +190,7 @@ impl BeaconChain { pub fn get_block_headers(&self, roots: &[Hash256]) -> Result, Error> { let headers: Result, _> = roots .iter() - .map(|root| match self.get_block(root)? { + .map(|root| match self.block_at_root(*root)? { Some(block) => Ok(block.block_header()), None => Err(Error::DBInconsistent("Missing block".into())), }) @@ -241,11 +241,29 @@ impl BeaconChain { /// ## Errors /// /// May return a database error. - pub fn get_block( + pub fn block_at_root( &self, - block_root: &Hash256, + block_root: Hash256, ) -> Result>, Error> { - Ok(self.store.get(block_root)?) + Ok(self.store.get(&block_root)?) + } + + /// Returns the block at the given slot, if any. Only returns blocks in the canonical chain. + /// + /// ## Errors + /// + /// May return a database error. + pub fn block_at_slot(&self, slot: Slot) -> Result>, Error> { + let root = self + .rev_iter_block_roots() + .find(|(_, this_slot)| *this_slot == slot) + .map(|(root, _)| root); + + if let Some(block_root) = root { + Ok(self.store.get(&block_root)?) + } else { + Ok(None) + } } /// Returns a `Checkpoint` representing the head block and state. Contains the "best block"; diff --git a/beacon_node/rest_api/Cargo.toml b/beacon_node/rest_api/Cargo.toml index d19317a719..82bf535b75 100644 --- a/beacon_node/rest_api/Cargo.toml +++ b/beacon_node/rest_api/Cargo.toml @@ -24,7 +24,6 @@ state_processing = { path = "../../eth2/state_processing" } types = { path = "../../eth2/types" } clap = "2.32.0" http = "^0.1.17" -prometheus = { version = "^0.6", features = ["process"] } hyper = "0.12.35" exit-future = "0.1.3" tokio = "0.1.17" @@ -36,3 +35,8 @@ slot_clock = { path = "../../eth2/utils/slot_clock" } hex = "0.3.2" parking_lot = "0.9" futures = "0.1.25" + +[dev-dependencies] +remote_beacon_node = { path = "../../eth2/utils/remote_beacon_node" } +node_test_rig = { path = "../../tests/node_test_rig" } +tree_hash = { path = "../../eth2/utils/tree_hash" } diff --git a/beacon_node/rest_api/src/lib.rs b/beacon_node/rest_api/src/lib.rs index 9165ccaac1..ad27cde3e7 100644 --- a/beacon_node/rest_api/src/lib.rs +++ b/beacon_node/rest_api/src/lib.rs @@ -27,7 +27,8 @@ use hyper::rt::Future; use hyper::service::Service; use hyper::{Body, Method, Request, Response, Server}; use parking_lot::RwLock; -use slog::{info, o, warn}; +use slog::{info, warn}; +use std::net::SocketAddr; use std::ops::Deref; use std::path::PathBuf; use std::sync::Arc; @@ -37,7 +38,7 @@ use url_query::UrlQuery; pub use crate::helpers::parse_pubkey; pub use beacon::{BlockResponse, HeadResponse, StateResponse}; -pub use config::Config as ApiConfig; +pub use config::Config; pub use validator::ValidatorDuty; type BoxFut = Box, Error = ApiError> + Send>; @@ -137,19 +138,17 @@ impl Service for ApiService { } // Methods for Validator - (&Method::GET, "/beacon/validator/duties") => { + (&Method::GET, "/validator/duties") => { into_boxfut(validator::get_validator_duties::(req)) } - (&Method::GET, "/beacon/validator/block") => { + (&Method::GET, "/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") => { + (&Method::POST, "/validator/block") => validator::publish_beacon_block::(req), + (&Method::GET, "/validator/attestation") => { into_boxfut(validator::get_new_attestation::(req)) } - (&Method::POST, "/beacon/validator/attestation") => { - validator::publish_attestation::(req) - } + (&Method::POST, "/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)), @@ -199,16 +198,14 @@ impl Service for ApiService { } pub fn start_server( - config: &ApiConfig, + config: &Config, executor: &TaskExecutor, beacon_chain: Arc>, network_info: NetworkInfo, db_path: PathBuf, eth2_config: Eth2Config, - log: &slog::Logger, -) -> Result { - let log = log.new(o!("Service" => "Api")); - + log: slog::Logger, +) -> Result<(exit_future::Signal, SocketAddr), hyper::Error> { // build a channel to kill the HTTP server let (exit_signal, exit) = exit_future::signal(); @@ -240,8 +237,11 @@ pub fn start_server( }; let log_clone = log.clone(); - let server = Server::bind(&bind_addr) - .serve(service) + let server = Server::bind(&bind_addr).serve(service); + + let actual_listen_addr = server.local_addr(); + + let server_future = server .with_graceful_shutdown(server_exit) .map_err(move |e| { warn!( @@ -251,15 +251,15 @@ pub fn start_server( }); info!( - log, - "REST API started"; - "address" => format!("{}", config.listen_address), - "port" => config.port, + log, + "REST API started"; + "address" => format!("{}", actual_listen_addr.ip()), + "port" => actual_listen_addr.port(), ); - executor.spawn(server); + executor.spawn(server_future); - Ok(exit_signal) + Ok((exit_signal, actual_listen_addr)) } #[derive(Clone)] diff --git a/beacon_node/rest_api/src/metrics.rs b/beacon_node/rest_api/src/metrics.rs index e9d98434eb..966d4deacf 100644 --- a/beacon_node/rest_api/src/metrics.rs +++ b/beacon_node/rest_api/src/metrics.rs @@ -3,7 +3,7 @@ use crate::response_builder::ResponseBuilder; use crate::{ApiError, ApiResult, DBPath}; use beacon_chain::BeaconChainTypes; use hyper::{Body, Request}; -use prometheus::{Encoder, TextEncoder}; +use lighthouse_metrics::{Encoder, TextEncoder}; pub use lighthouse_metrics::*; @@ -58,7 +58,7 @@ pub fn get_prometheus(req: Request) -> ApiR beacon_chain::scrape_for_metrics(&beacon_chain); encoder - .encode(&lighthouse_metrics::gather(), &mut buffer) + .encode(&lighthouse_metrics::gather()[..], &mut buffer) .unwrap(); String::from_utf8(buffer) diff --git a/beacon_node/rest_api/tests/test.rs b/beacon_node/rest_api/tests/test.rs new file mode 100644 index 0000000000..1ec75c8419 --- /dev/null +++ b/beacon_node/rest_api/tests/test.rs @@ -0,0 +1,161 @@ +#![cfg(test)] + +use node_test_rig::{ + environment::{Environment, EnvironmentBuilder}, + LocalBeaconNode, +}; +use tree_hash::TreeHash; +use types::{ + test_utils::generate_deterministic_keypair, Domain, EthSpec, MinimalEthSpec, Signature, Slot, +}; + +type E = MinimalEthSpec; + +fn build_env() -> Environment { + EnvironmentBuilder::minimal() + .null_logger() + .expect("should build env logger") + .single_thread_tokio_runtime() + .expect("should start tokio runtime") + .build() + .expect("environment should build") +} + +#[test] +fn validator_block() { + let mut env = build_env(); + + let spec = E::default_spec(); + + let node = LocalBeaconNode::production(env.core_context()); + let remote_node = node.remote_node().expect("should produce remote node"); + + let beacon_chain = node + .client + .beacon_chain() + .expect("client should have beacon chain"); + + let fork = beacon_chain.head().beacon_state.fork.clone(); + + let slot = Slot::new(1); + let randao_reveal = { + let proposer_index = beacon_chain + .block_proposer(slot) + .expect("should get proposer index"); + let keypair = generate_deterministic_keypair(proposer_index); + let epoch = slot.epoch(E::slots_per_epoch()); + let message = epoch.tree_hash_root(); + let domain = spec.get_domain(epoch, Domain::Randao, &fork); + Signature::new(&message, domain, &keypair.sk) + }; + + let block = env + .runtime() + .block_on( + remote_node + .http + .validator() + .block(slot, randao_reveal.clone()), + ) + .expect("should fetch block from http api"); + + let (expected_block, _state) = node + .client + .beacon_chain() + .expect("client should have beacon chain") + .produce_block(randao_reveal, slot) + .expect("should produce block"); + + assert_eq!( + block, expected_block, + "the block returned from the API should be as expected" + ); +} + +#[test] +fn beacon_state() { + let mut env = build_env(); + + let node = LocalBeaconNode::production(env.core_context()); + let remote_node = node.remote_node().expect("should produce remote node"); + + let (state_by_slot, root) = env + .runtime() + .block_on(remote_node.http.beacon().state_by_slot(Slot::new(0))) + .expect("should fetch state from http api"); + + let (state_by_root, root_2) = env + .runtime() + .block_on(remote_node.http.beacon().state_by_root(root)) + .expect("should fetch state from http api"); + + let mut db_state = node + .client + .beacon_chain() + .expect("client should have beacon chain") + .state_at_slot(Slot::new(0)) + .expect("should find state"); + db_state.drop_all_caches(); + + assert_eq!( + root, root_2, + "the two roots returned from the api should be identical" + ); + assert_eq!( + root, + db_state.canonical_root(), + "root from database should match that from the API" + ); + assert_eq!( + state_by_slot, db_state, + "genesis state by slot from api should match that from the DB" + ); + assert_eq!( + state_by_root, db_state, + "genesis state by root from api should match that from the DB" + ); +} + +#[test] +fn beacon_block() { + let mut env = build_env(); + + let node = LocalBeaconNode::production(env.core_context()); + let remote_node = node.remote_node().expect("should produce remote node"); + + let (block_by_slot, root) = env + .runtime() + .block_on(remote_node.http.beacon().block_by_slot(Slot::new(0))) + .expect("should fetch block from http api"); + + let (block_by_root, root_2) = env + .runtime() + .block_on(remote_node.http.beacon().block_by_root(root)) + .expect("should fetch block from http api"); + + let db_block = node + .client + .beacon_chain() + .expect("client should have beacon chain") + .block_at_slot(Slot::new(0)) + .expect("should find block") + .expect("block should not be none"); + + assert_eq!( + root, root_2, + "the two roots returned from the api should be identical" + ); + assert_eq!( + root, + db_block.canonical_root(), + "root from database should match that from the API" + ); + assert_eq!( + block_by_slot, db_block, + "genesis block by slot from api should match that from the DB" + ); + assert_eq!( + block_by_root, db_block, + "genesis block by root from api should match that from the DB" + ); +} diff --git a/eth2/utils/lighthouse_metrics/src/lib.rs b/eth2/utils/lighthouse_metrics/src/lib.rs index 225bb460b6..7c229b5929 100644 --- a/eth2/utils/lighthouse_metrics/src/lib.rs +++ b/eth2/utils/lighthouse_metrics/src/lib.rs @@ -55,7 +55,7 @@ use prometheus::{HistogramOpts, HistogramTimer, Opts}; -pub use prometheus::{Histogram, IntCounter, IntGauge, Result}; +pub use prometheus::{Encoder, Histogram, IntCounter, IntGauge, Result, TextEncoder}; /// Collect all the metrics for reporting. pub fn gather() -> Vec { diff --git a/eth2/utils/remote_beacon_node/Cargo.toml b/eth2/utils/remote_beacon_node/Cargo.toml index 48567de37f..677646b4d4 100644 --- a/eth2/utils/remote_beacon_node/Cargo.toml +++ b/eth2/utils/remote_beacon_node/Cargo.toml @@ -12,3 +12,6 @@ url = "1.2" serde = "1.0" futures = "0.1.25" types = { path = "../../../eth2/types" } +rest_api = { path = "../../../beacon_node/rest_api" } +hex = "0.3" +eth2_ssz = { path = "../../../eth2/utils/ssz" } diff --git a/eth2/utils/remote_beacon_node/src/lib.rs b/eth2/utils/remote_beacon_node/src/lib.rs index a796e166a5..1ef0c9cc38 100644 --- a/eth2/utils/remote_beacon_node/src/lib.rs +++ b/eth2/utils/remote_beacon_node/src/lib.rs @@ -6,9 +6,10 @@ use futures::{Future, IntoFuture}; use reqwest::r#async::{Client, RequestBuilder}; use serde::Deserialize; +use ssz::Encode; use std::marker::PhantomData; use std::net::SocketAddr; -use types::{BeaconBlock, BeaconState, EthSpec}; +use types::{BeaconBlock, BeaconState, EthSpec, Signature}; use types::{Hash256, Slot}; use url::Url; @@ -53,16 +54,55 @@ impl HttpClient { Beacon(self.clone()) } + pub fn validator(&self) -> Validator { + Validator(self.clone()) + } + fn url(&self, path: &str) -> Result { self.url.join(path).map_err(|e| e.into()) } pub fn get(&self, path: &str) -> Result { + // TODO: add timeout self.url(path) .map(|url| Client::new().get(&url.to_string())) } } +/// Provides the functions on the `/beacon` endpoint of the node. +#[derive(Clone)] +pub struct Validator(HttpClient); + +impl Validator { + fn url(&self, path: &str) -> Result { + self.0 + .url("validator/") + .and_then(move |url| url.join(path).map_err(Error::from)) + .map_err(Into::into) + } + + /// Requests a new (unsigned) block from the beacon node. + pub fn block( + &self, + slot: Slot, + randao_reveal: Signature, + ) -> impl Future, Error = Error> { + let client = self.0.clone(); + self.url("block") + .into_future() + .and_then(move |mut url| { + url.query_pairs_mut() + .append_pair("slot", &format!("{}", slot.as_u64())); + url.query_pairs_mut() + .append_pair("randao_reveal", &signature_as_string(&randao_reveal)); + client.get(&url.to_string()) + }) + .and_then(|builder| builder.send().map_err(Error::from)) + .and_then(|response| response.error_for_status().map_err(Error::from)) + .and_then(|mut success| success.json::>().map_err(Error::from)) + } +} + /// Provides the functions on the `/beacon` endpoint of the node. #[derive(Clone)] pub struct Beacon(HttpClient); @@ -76,16 +116,32 @@ impl Beacon { } /// Returns the block and block root at the given slot. - pub fn block_at_slot( + pub fn block_by_slot( &self, slot: Slot, + ) -> impl Future, Hash256), Error = Error> { + self.block("slot", format!("{}", slot.as_u64())) + } + + /// Returns the block and block root at the given root. + pub fn block_by_root( + &self, + root: Hash256, + ) -> impl Future, Hash256), Error = Error> { + self.block("root", root_as_string(root)) + } + + /// Returns the block and block root at the given slot. + fn block( + &self, + query_key: &'static str, + query_param: String, ) -> impl Future, Hash256), Error = Error> { let client = self.0.clone(); self.url("block") .into_future() .and_then(move |mut url| { - url.query_pairs_mut() - .append_pair("slot", &format!("{}", slot.as_u64())); + url.query_pairs_mut().append_pair(query_key, &query_param); client.get(&url.to_string()) }) .and_then(|builder| builder.send().map_err(Error::from)) @@ -95,16 +151,32 @@ impl Beacon { } /// Returns the state and state root at the given slot. - pub fn state_at_slot( + pub fn state_by_slot( &self, slot: Slot, + ) -> impl Future, Hash256), Error = Error> { + self.state("slot", format!("{}", slot.as_u64())) + } + + /// Returns the state and state root at the given root. + pub fn state_by_root( + &self, + root: Hash256, + ) -> impl Future, Hash256), Error = Error> { + self.state("root", root_as_string(root)) + } + + /// Returns the state and state root at the given slot. + fn state( + &self, + query_key: &'static str, + query_param: String, ) -> impl Future, Hash256), Error = Error> { let client = self.0.clone(); self.url("state") .into_future() .and_then(move |mut url| { - url.query_pairs_mut() - .append_pair("slot", &format!("{}", slot.as_u64())); + url.query_pairs_mut().append_pair(query_key, &query_param); client.get(&url.to_string()) }) .and_then(|builder| builder.send().map_err(Error::from)) @@ -128,6 +200,14 @@ pub struct StateResponse { pub root: Hash256, } +fn root_as_string(root: Hash256) -> String { + format!("0x{:?}", root) +} + +fn signature_as_string(signature: &Signature) -> String { + format!("0x{}", hex::encode(signature.as_ssz_bytes())) +} + impl From for Error { fn from(e: reqwest::Error) -> Error { Error::ReqwestError(e)