From 989e2727d76a12cd4defbd2043f99d173026b25c Mon Sep 17 00:00:00 2001 From: Paul Hauner Date: Sat, 10 Aug 2019 17:15:15 +1000 Subject: [PATCH] Changes to rest_api (#480) * Add half-finished rest api changes * Add basic, messy changes to rest api * Fix expect() in ApiRequest * Remove ApiRequest, add route for beacon state * Tidy rest api, add get state from root * Add api method for getting state roots by slot * Add test for URL helper * Simplify state_at_slot fn * Add tests for rest api helper fns * Add extra tests for parse root * Fix clippy lints * Fix compile error in rest api * Update test to new ethereum-types --- beacon_node/beacon_chain/src/beacon_chain.rs | 2 +- beacon_node/client/src/config.rs | 4 +- beacon_node/rest_api/Cargo.toml | 5 +- beacon_node/rest_api/src/beacon.rs | 60 +++++++ beacon_node/rest_api/src/beacon_node.rs | 65 -------- beacon_node/rest_api/src/helpers.rs | 156 +++++++++++++++++++ beacon_node/rest_api/src/lib.rs | 142 ++++++++++------- beacon_node/rest_api/src/macros.rs | 23 --- beacon_node/rest_api/src/node.rs | 25 +++ beacon_node/rest_api/src/url_query.rs | 112 +++++++++++++ beacon_node/src/main.rs | 1 - beacon_node/store/src/iter.rs | 9 ++ 12 files changed, 451 insertions(+), 153 deletions(-) create mode 100644 beacon_node/rest_api/src/beacon.rs delete mode 100644 beacon_node/rest_api/src/beacon_node.rs create mode 100644 beacon_node/rest_api/src/helpers.rs delete mode 100644 beacon_node/rest_api/src/macros.rs create mode 100644 beacon_node/rest_api/src/node.rs create mode 100644 beacon_node/rest_api/src/url_query.rs diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index 34902b215..28ba5fe48 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -296,7 +296,7 @@ impl BeaconChain { /// It is important to note that the `beacon_state` returned may not match the present slot. It /// is the state as it was when the head block was received, which could be some slots prior to /// now. - pub fn head(&self) -> RwLockReadGuard> { + pub fn head<'a>(&'a self) -> RwLockReadGuard<'a, CheckPoint> { self.canonical_head.read() } diff --git a/beacon_node/client/src/config.rs b/beacon_node/client/src/config.rs index 176625d77..ee62b6281 100644 --- a/beacon_node/client/src/config.rs +++ b/beacon_node/client/src/config.rs @@ -26,7 +26,7 @@ pub struct Config { pub network: network::NetworkConfig, pub rpc: rpc::RPCConfig, pub http: HttpServerConfig, - pub rest_api: rest_api::APIConfig, + pub rest_api: rest_api::ApiConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -60,7 +60,7 @@ impl Default for Config { network: NetworkConfig::new(), rpc: rpc::RPCConfig::default(), http: HttpServerConfig::default(), - rest_api: rest_api::APIConfig::default(), + rest_api: rest_api::ApiConfig::default(), spec_constants: TESTNET_SPEC_CONSTANTS.into(), genesis_state: GenesisState::RecentGenesis { validator_count: TESTNET_VALIDATOR_COUNT, diff --git a/beacon_node/rest_api/Cargo.toml b/beacon_node/rest_api/Cargo.toml index 7a63ca036..fb6cb8413 100644 --- a/beacon_node/rest_api/Cargo.toml +++ b/beacon_node/rest_api/Cargo.toml @@ -7,16 +7,19 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] beacon_chain = { path = "../beacon_chain" } +store = { path = "../store" } version = { path = "../version" } serde = { version = "1.0", features = ["derive"] } serde_json = "^1.0" slog = "^2.2.3" slog-term = "^2.4.0" slog-async = "^2.3.0" +state_processing = { path = "../../eth2/state_processing" } +types = { path = "../../eth2/types" } clap = "2.32.0" http = "^0.1.17" hyper = "0.12.32" -hyper-router = "^0.5" futures = "0.1" exit-future = "0.1.3" tokio = "0.1.17" +url = "2.0" diff --git a/beacon_node/rest_api/src/beacon.rs b/beacon_node/rest_api/src/beacon.rs new file mode 100644 index 000000000..cef23abe8 --- /dev/null +++ b/beacon_node/rest_api/src/beacon.rs @@ -0,0 +1,60 @@ +use super::{success_response, ApiResult}; +use crate::{helpers::*, ApiError, UrlQuery}; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use hyper::{Body, Request}; +use std::sync::Arc; +use store::Store; +use types::BeaconState; + +/// HTTP handler to return a `BeaconState` at a given `root` or `slot`. +/// +/// 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 query_params = ["root", "slot"]; + let (key, value) = UrlQuery::from_request(&req)?.first_of(&query_params)?; + + let state: BeaconState = match (key.as_ref(), value) { + ("slot", value) => state_at_slot(&beacon_chain, parse_slot(&value)?)?, + ("root", value) => { + let root = &parse_root(&value)?; + + beacon_chain + .store + .get(root)? + .ok_or_else(|| ApiError::NotFound(format!("No state for root: {}", root)))? + } + _ => unreachable!("Guarded by UrlQuery::from_request()"), + }; + + let json: String = serde_json::to_string(&state) + .map_err(|e| ApiError::ServerError(format!("Unable to serialize BeaconState: {:?}", e)))?; + + Ok(success_response(Body::from(json))) +} + +/// HTTP handler to return a `BeaconState` root at a given or `slot`. +/// +/// 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 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))) +} diff --git a/beacon_node/rest_api/src/beacon_node.rs b/beacon_node/rest_api/src/beacon_node.rs deleted file mode 100644 index bd8d98a53..000000000 --- a/beacon_node/rest_api/src/beacon_node.rs +++ /dev/null @@ -1,65 +0,0 @@ -use beacon_chain::{BeaconChain, BeaconChainTypes}; -use serde::Serialize; -use slog::info; -use std::sync::Arc; -use version; - -use super::{path_from_request, success_response, APIResult, APIService}; - -use hyper::{Body, Request, Response}; -use hyper_router::{Route, RouterBuilder}; - -#[derive(Clone)] -pub struct BeaconNodeServiceInstance { - pub marker: std::marker::PhantomData, -} - -/// A string which uniquely identifies the client implementation and its version; similar to [HTTP User-Agent](https://tools.ietf.org/html/rfc7231#section-5.5.3). -#[derive(Serialize)] -pub struct Version(String); -impl From for Version { - fn from(x: String) -> Self { - Version(x) - } -} - -/// The genesis_time configured for the beacon node, which is the unix time at which the Eth2.0 chain began. -#[derive(Serialize)] -pub struct GenesisTime(u64); -impl From for GenesisTime { - fn from(x: u64) -> Self { - GenesisTime(x) - } -} - -impl APIService for BeaconNodeServiceInstance { - fn add_routes(&mut self, router_builder: RouterBuilder) -> Result { - let router_builder = router_builder - .add(Route::get("/version").using(result_to_response!(get_version))) - .add(Route::get("/genesis_time").using(result_to_response!(get_genesis_time::))); - Ok(router_builder) - } -} - -/// Read the version string from the current Lighthouse build. -fn get_version(_req: Request) -> APIResult { - let ver = Version::from(version::version()); - let body = Body::from( - serde_json::to_string(&ver).expect("Version should always be serialializable as JSON."), - ); - Ok(success_response(body)) -} - -/// Read the genesis time from the current beacon chain state. -fn get_genesis_time(req: Request) -> APIResult { - let beacon_chain = req.extensions().get::>>().unwrap(); - let gen_time = { - let state = &beacon_chain.head().beacon_state; - 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)) -} diff --git a/beacon_node/rest_api/src/helpers.rs b/beacon_node/rest_api/src/helpers.rs new file mode 100644 index 000000000..2a429076c --- /dev/null +++ b/beacon_node/rest_api/src/helpers.rs @@ -0,0 +1,156 @@ +use crate::ApiError; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use store::{iter::AncestorIter, Store}; +use types::{BeaconState, EthSpec, Hash256, RelativeEpoch, Slot}; + +/// Parse a slot from a `0x` preixed string. +/// +/// E.g., `"1234"` +pub fn parse_slot(string: &str) -> Result { + string + .parse::() + .map(Slot::from) + .map_err(|e| ApiError::InvalidQueryParams(format!("Unable to parse slot: {:?}", e))) +} + +/// Parse a root from a `0x` preixed string. +/// +/// E.g., `"0x0000000000000000000000000000000000000000000000000000000000000000"` +pub fn parse_root(string: &str) -> Result { + const PREFIX: &str = "0x"; + + if string.starts_with(PREFIX) { + let trimmed = string.trim_start_matches(PREFIX); + trimmed + .parse() + .map_err(|e| ApiError::InvalidQueryParams(format!("Unable to parse root: {:?}", e))) + } else { + Err(ApiError::InvalidQueryParams( + "Root must have a '0x' prefix".to_string(), + )) + } +} + +/// Returns a `BeaconState` in the canonical chain of `beacon_chain` at the given `slot`, if +/// possible. +/// +/// 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 state_at_slot( + beacon_chain: &BeaconChain, + slot: Slot, +) -> Result, ApiError> { + let head_state = &beacon_chain.head().beacon_state; + + if head_state.slot == slot { + // The request slot is the same as the best block (head) slot. + + // I'm not sure if this `.clone()` will be optimized out. If not, it seems unnecessary. + Ok(beacon_chain.head().beacon_state.clone()) + } else { + let root = state_root_at_slot(beacon_chain, slot)?; + + let state: BeaconState = beacon_chain + .store + .get(&root)? + .ok_or_else(|| ApiError::NotFound(format!("Unable to find state at root {}", root)))?; + + Ok(state) + } +} + +/// Returns the root of the `BeaconState` in the canonical chain of `beacon_chain` at the given +/// `slot`, if possible. +/// +/// Will not return a state root if the request slot is in the future. Will return state roots +/// higher than the current head by skipping slots. +pub fn state_root_at_slot( + beacon_chain: &BeaconChain, + slot: Slot, +) -> Result { + let head_state = &beacon_chain.head().beacon_state; + let current_slot = beacon_chain + .read_slot_clock() + .ok_or_else(|| ApiError::ServerError("Unable to read slot clock".to_string()))?; + + // There are four scenarios when obtaining a state for a given slot: + // + // 1. The request slot is in the future. + // 2. The request slot is the same as the best block (head) slot. + // 3. The request slot is prior to the head slot. + // 4. The request slot is later than the head slot. + if current_slot < slot { + // 1. The request slot is in the future. Reject the request. + // + // 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!( + "Requested slot {} is past the current slot {}", + slot, current_slot + ))) + } else if head_state.slot == slot { + // 2. The request slot is the same as the best block (head) slot. + // + // The head state root is stored in memory, return a reference. + Ok(beacon_chain.head().beacon_state_root) + } else if head_state.slot > slot { + // 3. The request slot is prior to the head slot. + // + // Iterate through the state roots on the head state to find the root for that + // slot. Once the root is found, load it from the database. + Ok(head_state + .try_iter_ancestor_roots(beacon_chain.store.clone()) + .ok_or_else(|| ApiError::ServerError("Failed to create roots iterator".to_string()))? + .find(|(_root, s)| *s == slot) + .map(|(root, _slot)| root) + .ok_or_else(|| ApiError::NotFound(format!("Unable to find state at slot {}", slot)))?) + } else { + // 4. The request slot is later than the head slot. + // + // Use `per_slot_processing` to advance the head state to the present slot, + // assuming that all slots do not contain a block (i.e., they are skipped slots). + let mut state = beacon_chain.head().beacon_state.clone(); + let spec = &T::EthSpec::default_spec(); + + for _ in state.slot.as_u64()..slot.as_u64() { + // Ensure the next epoch state caches are built in case of an epoch transition. + state.build_committee_cache(RelativeEpoch::Next, spec)?; + + state_processing::per_slot_processing(&mut state, spec)?; + } + + // Note: this is an expensive operation. Once the tree hash cache is implement it may be + // used here. + Ok(state.canonical_root()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parse_root_works() { + assert_eq!( + parse_root("0x0000000000000000000000000000000000000000000000000000000000000000"), + Ok(Hash256::zero()) + ); + assert_eq!( + parse_root("0x000000000000000000000000000000000000000000000000000000000000002a"), + Ok(Hash256::from_low_u64_be(42)) + ); + assert!( + parse_root("0000000000000000000000000000000000000000000000000000000000000042").is_err() + ); + assert!(parse_root("0x").is_err()); + assert!(parse_root("0x00").is_err()); + } + + #[test] + fn parse_slot_works() { + assert_eq!(parse_slot("0"), Ok(Slot::new(0))); + assert_eq!(parse_slot("42"), Ok(Slot::new(42))); + assert_eq!(parse_slot("10000000"), Ok(Slot::new(10_000_000))); + assert!(parse_slot("cats").is_err()); + } +} diff --git a/beacon_node/rest_api/src/lib.rs b/beacon_node/rest_api/src/lib.rs index 0f7849449..a94a8cdf4 100644 --- a/beacon_node/rest_api/src/lib.rs +++ b/beacon_node/rest_api/src/lib.rs @@ -1,37 +1,42 @@ extern crate futures; extern crate hyper; -#[macro_use] -mod macros; -mod beacon_node; -pub mod config; +mod beacon; +mod config; +mod helpers; +mod node; +mod url_query; use beacon_chain::{BeaconChain, BeaconChainTypes}; -pub use config::Config as APIConfig; - +pub use config::Config as ApiConfig; +use hyper::rt::Future; +use hyper::service::service_fn_ok; +use hyper::{Body, Method, Response, Server, StatusCode}; use slog::{info, o, warn}; use std::sync::Arc; use tokio::runtime::TaskExecutor; +use url_query::UrlQuery; -use crate::beacon_node::BeaconNodeServiceInstance; -use hyper::rt::Future; -use hyper::service::{service_fn, Service}; -use hyper::{Body, Request, Response, Server, StatusCode}; -use hyper_router::{RouterBuilder, RouterService}; - -pub enum APIError { - MethodNotAllowed { desc: String }, - ServerError { desc: String }, - NotImplemented { desc: String }, +#[derive(PartialEq, Debug)] +pub enum ApiError { + MethodNotAllowed(String), + ServerError(String), + NotImplemented(String), + InvalidQueryParams(String), + NotFound(String), + ImATeapot(String), // Just in case. } -pub type APIResult = Result, APIError>; +pub type ApiResult = Result, ApiError>; -impl Into> for APIError { +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::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), }; Response::builder() .status(status_code.0) @@ -40,17 +45,31 @@ impl Into> for APIError { } } -pub trait APIService { - fn add_routes(&mut self, router_builder: RouterBuilder) -> Result; +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)) + } } pub fn start_server( - config: &APIConfig, + config: &ApiConfig, executor: &TaskExecutor, beacon_chain: Arc>, log: &slog::Logger, ) -> Result { - let log = log.new(o!("Service" => "API")); + let log = log.new(o!("Service" => "Api")); // build a channel to kill the HTTP server let (exit_signal, exit) = exit_future::signal(); @@ -68,62 +87,65 @@ pub fn start_server( let server_log = log.clone(); let server_bc = beacon_chain.clone(); - // Create the service closure let service = move || { - //TODO: This router must be moved out of this closure, so it isn't rebuilt for every connection. - let mut router = build_router_service::(); - - // Clone our stateful objects, for use in handler closure - let service_log = server_log.clone(); - let service_bc = server_bc.clone(); + let log = server_log.clone(); + let beacon_chain = server_bc.clone(); // Create a simple handler for the router, inject our stateful objects into the request. - service_fn(move |mut req| { + service_fn_ok(move |mut req| { + req.extensions_mut().insert::(log.clone()); req.extensions_mut() - .insert::(service_log.clone()); - req.extensions_mut() - .insert::>>(service_bc.clone()); - router.call(req) + .insert::>>(beacon_chain.clone()); + + let path = req.uri().path().to_string(); + + // Route the request to the correct handler. + let result = match (req.method(), path.as_ref()) { + (&Method::GET, "/beacon/state") => beacon::get_state::(req), + (&Method::GET, "/beacon/state_root") => beacon::get_state_root::(req), + (&Method::GET, "/node/version") => node::get_version(req), + (&Method::GET, "/node/genesis_time") => node::get_genesis_time::(req), + _ => Err(ApiError::MethodNotAllowed(path.clone())), + }; + + match result { + // Return the `hyper::Response`. + Ok(response) => { + slog::debug!(log, "Request successful: {:?}", path); + response + } + // Map the `ApiError` into `hyper::Response`. + Err(e) => { + slog::debug!(log, "Request failure: {:?}", path); + e.into() + } + } }) }; + let log_clone = log.clone(); let server = Server::bind(&bind_addr) .serve(service) .with_graceful_shutdown(server_exit) .map_err(move |e| { warn!( - log, + 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, + ); + executor.spawn(server); Ok(exit_signal) } -fn build_router_service() -> RouterService { - let mut router_builder = RouterBuilder::new(); - - let mut bn_service: BeaconNodeServiceInstance = BeaconNodeServiceInstance { - marker: std::marker::PhantomData, - }; - - router_builder = bn_service - .add_routes(router_builder) - .expect("The routes should always be made."); - - RouterService::new(router_builder.build()) -} - -fn path_from_request(req: &Request) -> String { - req.uri() - .path_and_query() - .as_ref() - .map(|pq| String::from(pq.as_str())) - .unwrap_or(String::new()) -} - fn success_response(body: Body) -> Response { Response::builder() .status(StatusCode::OK) diff --git a/beacon_node/rest_api/src/macros.rs b/beacon_node/rest_api/src/macros.rs deleted file mode 100644 index db9bfd848..000000000 --- a/beacon_node/rest_api/src/macros.rs +++ /dev/null @@ -1,23 +0,0 @@ -macro_rules! result_to_response { - ($handler: path) => { - |req: Request| -> Response { - let log = req - .extensions() - .get::() - .expect("Our logger should be on req.") - .clone(); - let path = path_from_request(&req); - let result = $handler(req); - match result { - Ok(response) => { - info!(log, "Request successful: {:?}", path); - response - } - Err(e) => { - info!(log, "Request failure: {:?}", path); - e.into() - } - } - } - }; -} diff --git a/beacon_node/rest_api/src/node.rs b/beacon_node/rest_api/src/node.rs new file mode 100644 index 000000000..4dbd41229 --- /dev/null +++ b/beacon_node/rest_api/src/node.rs @@ -0,0 +1,25 @@ +use crate::{success_response, ApiResult}; +use beacon_chain::{BeaconChain, 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)) +} + +/// 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)) +} diff --git a/beacon_node/rest_api/src/url_query.rs b/beacon_node/rest_api/src/url_query.rs new file mode 100644 index 000000000..d65312a9e --- /dev/null +++ b/beacon_node/rest_api/src/url_query.rs @@ -0,0 +1,112 @@ +use crate::ApiError; +use hyper::Request; + +/// Provides handy functions for parsing the query parameters of a URL. +pub struct UrlQuery<'a>(url::form_urlencoded::Parse<'a>); + +impl<'a> UrlQuery<'a> { + /// Instantiate from an existing `Request`. + /// + /// 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( + "URL query must be valid and contain at least one + key." + .to_string(), + ) + })?; + + Ok(UrlQuery(url::form_urlencoded::parse(query_str.as_bytes()))) + } + + /// Returns the first `(key, value)` pair found where the `key` is in `keys`. + /// + /// If no match is found, an `InvalidQueryParams` error is returned. + pub fn first_of(mut self, keys: &[&str]) -> Result<(String, String), ApiError> { + self.0 + .find(|(key, _value)| keys.contains(&&**key)) + .map(|(key, value)| (key.into_owned(), value.into_owned())) + .ok_or_else(|| { + ApiError::InvalidQueryParams(format!( + "URL query must contain at least one of the following keys: {:?}", + keys + )) + }) + } + + /// Returns the value for `key`, if and only if `key` is the only key present in the query + /// parameters. + pub fn only_one(self, key: &str) -> Result { + let queries: Vec<_> = self + .0 + .map(|(k, v)| (k.into_owned(), v.into_owned())) + .collect(); + + if queries.len() == 1 { + let (first_key, first_value) = &queries[0]; // Must have 0 index if len is 1. + if first_key == key { + Ok(first_value.to_string()) + } else { + Err(ApiError::InvalidQueryParams(format!( + "Only the {} query parameter is supported", + key + ))) + } + } else { + Err(ApiError::InvalidQueryParams(format!( + "Only one query parameter is allowed, {} supplied", + queries.len() + ))) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn only_one() { + let get_result = |addr: &str, key: &str| -> Result { + UrlQuery(url::Url::parse(addr).unwrap().query_pairs()).only_one(key) + }; + + assert_eq!(get_result("http://cat.io/?a=42", "a"), Ok("42".to_string())); + assert!(get_result("http://cat.io/?a=42", "b").is_err()); + assert!(get_result("http://cat.io/?a=42&b=12", "a").is_err()); + assert!(get_result("http://cat.io/", "").is_err()); + } + + #[test] + fn first_of() { + let url = url::Url::parse("http://lighthouse.io/cats?a=42&b=12&c=100").unwrap(); + let get_query = || UrlQuery(url.query_pairs()); + + assert_eq!( + get_query().first_of(&["a"]), + Ok(("a".to_string(), "42".to_string())) + ); + assert_eq!( + get_query().first_of(&["a", "b", "c"]), + Ok(("a".to_string(), "42".to_string())) + ); + assert_eq!( + get_query().first_of(&["a", "a", "a"]), + Ok(("a".to_string(), "42".to_string())) + ); + assert_eq!( + get_query().first_of(&["a", "b", "c"]), + Ok(("a".to_string(), "42".to_string())) + ); + assert_eq!( + get_query().first_of(&["b", "c"]), + Ok(("b".to_string(), "12".to_string())) + ); + assert_eq!( + get_query().first_of(&["c"]), + Ok(("c".to_string(), "100".to_string())) + ); + assert!(get_query().first_of(&["nothing"]).is_err()); + } +} diff --git a/beacon_node/src/main.rs b/beacon_node/src/main.rs index b34259f5a..2e3ad0691 100644 --- a/beacon_node/src/main.rs +++ b/beacon_node/src/main.rs @@ -210,7 +210,6 @@ fn main() { Arg::with_name("debug-level") .long("debug-level") .value_name("LEVEL") - .short("s") .help("The title of the spec constants for chain config.") .takes_value(true) .possible_values(&["info", "debug", "trace", "warn", "error", "crit"]) diff --git a/beacon_node/store/src/iter.rs b/beacon_node/store/src/iter.rs index c4e557b2d..4e47fceb2 100644 --- a/beacon_node/store/src/iter.rs +++ b/beacon_node/store/src/iter.rs @@ -24,6 +24,15 @@ impl<'a, U: Store, E: EthSpec> AncestorIter> for } } +impl<'a, U: Store, E: EthSpec> AncestorIter> for BeaconState { + /// Iterates across all the prior state roots of `self`, starting at the most recent and ending + /// at genesis. + fn try_iter_ancestor_roots(&self, store: Arc) -> Option> { + // The `self.clone()` here is wasteful. + Some(StateRootsIterator::owned(store, self.clone(), self.slot)) + } +} + #[derive(Clone)] pub struct StateRootsIterator<'a, T: EthSpec, U> { store: Arc,