From 588b90157d84eb288505b8a4e2addf663e2a5329 Mon Sep 17 00:00:00 2001 From: realbigsean Date: Wed, 6 Jan 2021 03:01:46 +0000 Subject: [PATCH] Ssz state api endpoint (#2111) ## Issue Addressed Catching up to a recently merged API spec PR: https://github.com/ethereum/eth2.0-APIs/pull/119 ## Proposed Changes - Return an SSZ beacon state on `/eth/v1/debug/beacon/states/{stateId}` when passed this header: `accept: application/octet-stream`. - requests to this endpoint with no `accept` header or an `accept` header and a value of `application/json` or `*/*` , or will result in a JSON response ## Additional Info Co-authored-by: realbigsean --- beacon_node/http_api/src/lib.rs | 36 +++++++++++++++----- beacon_node/http_api/tests/tests.rs | 10 ++++-- common/eth2/src/lib.rs | 53 +++++++++++++++++++++++++++++ common/eth2/src/types.rs | 31 +++++++++++++++++ 4 files changed, 120 insertions(+), 10 deletions(-) diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 0a625cdcc..a62276315 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -43,6 +43,7 @@ use types::{ }; use warp::http::StatusCode; use warp::sse::ServerSentEvent; +use warp::Reply; use warp::{http::Response, Filter, Stream}; use warp_utils::reject::ServerSentEventError; use warp_utils::task::{blocking_json_task, blocking_task}; @@ -1242,16 +1243,35 @@ pub fn serve( )) })) .and(warp::path::end()) + .and(warp::header::optional::("accept")) .and(chain_filter.clone()) - .and_then(|state_id: StateId, chain: Arc>| { - blocking_task(move || { - state_id.map_state(&chain, |state| { - Ok(warp::reply::json(&api_types::GenericResponseRef::from( - &state, - ))) + .and_then( + |state_id: StateId, + accept_header: Option, + chain: Arc>| { + blocking_task(move || match accept_header { + Some(api_types::Accept::Ssz) => { + let state = state_id.state(&chain)?; + Response::builder() + .status(200) + .header("Content-Type", "application/octet-stream") + .body(state.as_ssz_bytes().into()) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "failed to create response: {}", + e + )) + }) + } + _ => state_id.map_state(&chain, |state| { + Ok( + warp::reply::json(&api_types::GenericResponseRef::from(&state)) + .into_response(), + ) + }), }) - }) - }); + }, + ); // GET debug/beacon/heads let get_debug_beacon_heads = eth1_v1 diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 18d6c332d..dedfe400a 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -1374,7 +1374,12 @@ impl ApiTester { pub async fn test_get_debug_beacon_states(self) -> Self { for state_id in self.interesting_state_ids() { - let result = self + let result_ssz = self + .client + .get_debug_beacon_states_ssz(state_id) + .await + .unwrap(); + let result_json = self .client .get_debug_beacon_states(state_id) .await @@ -1384,7 +1389,8 @@ impl ApiTester { let mut expected = self.get_state(state_id); expected.as_mut().map(|state| state.drop_all_caches()); - assert_eq!(result, expected, "{:?}", state_id); + assert_eq!(result_ssz, expected, "{:?}", state_id); + assert_eq!(result_json, expected, "{:?}", state_id); } self diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 902d5fd28..94f1e7c5b 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -20,6 +20,7 @@ pub use reqwest; use reqwest::{IntoUrl, Response}; pub use reqwest::{StatusCode, Url}; use serde::{de::DeserializeOwned, Serialize}; +use ssz::Decode; use std::convert::TryFrom; use std::fmt; use std::iter::Iterator; @@ -144,6 +145,37 @@ impl BeaconNodeHttpClient { } } + /// Perform a HTTP GET request using an 'accept' header, returning `None` on a 404 error. + pub async fn get_bytes_opt_accept_header( + &self, + url: U, + accept_header: Accept, + ) -> Result>, Error> { + let response = self + .client + .get(url) + .header(ACCEPT, accept_header.to_string()) + .send() + .await + .map_err(Error::Reqwest)?; + match ok_or_error(response).await { + Ok(resp) => Ok(Some( + resp.bytes() + .await + .map_err(Error::Reqwest)? + .into_iter() + .collect::>(), + )), + Err(err) => { + if err.status() == Some(StatusCode::NOT_FOUND) { + Ok(None) + } else { + Err(err) + } + } + } + } + /// Perform a HTTP POST request. async fn post(&self, url: U, body: &T) -> Result<(), Error> { let response = self @@ -824,6 +856,27 @@ impl BeaconNodeHttpClient { self.get_opt(path).await } + /// `GET debug/beacon/states/{state_id}` + /// `-H "accept: application/octet-stream"` + pub async fn get_debug_beacon_states_ssz( + &self, + state_id: StateId, + ) -> Result>, Error> { + let mut path = self.eth_path()?; + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("debug") + .push("beacon") + .push("states") + .push(&state_id.to_string()); + + self.get_bytes_opt_accept_header(path, Accept::Ssz) + .await? + .map(|bytes| BeaconState::from_ssz_bytes(&bytes).map_err(Error::InvalidSsz)) + .transpose() + } + /// `GET debug/beacon/heads` pub async fn get_debug_beacon_heads( &self, diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index b37e3de43..41e705c72 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -3,6 +3,7 @@ use crate::Error as ServerError; use eth2_libp2p::{ConnectionDirection, Enr, Multiaddr, PeerConnectionStatus}; +pub use reqwest::header::ACCEPT; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; use std::fmt; @@ -768,6 +769,36 @@ impl fmt::Display for EventTopic { } } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Accept { + Json, + Ssz, + Any, +} + +impl fmt::Display for Accept { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Accept::Ssz => write!(f, "application/octet-stream"), + Accept::Json => write!(f, "application/json"), + Accept::Any => write!(f, "*/*"), + } + } +} + +impl FromStr for Accept { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "application/octet-stream" => Ok(Accept::Ssz), + "application/json" => Ok(Accept::Json), + "*/*" => Ok(Accept::Any), + _ => Err("accept header cannot be parsed.".to_string()), + } + } +} + #[cfg(test)] mod tests { use super::*;