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 <seananderson33@gmail.com>
This commit is contained in:
realbigsean 2021-01-06 03:01:46 +00:00
parent 939fa717fd
commit 588b90157d
4 changed files with 120 additions and 10 deletions

View File

@ -43,6 +43,7 @@ use types::{
}; };
use warp::http::StatusCode; use warp::http::StatusCode;
use warp::sse::ServerSentEvent; use warp::sse::ServerSentEvent;
use warp::Reply;
use warp::{http::Response, Filter, Stream}; use warp::{http::Response, Filter, Stream};
use warp_utils::reject::ServerSentEventError; use warp_utils::reject::ServerSentEventError;
use warp_utils::task::{blocking_json_task, blocking_task}; use warp_utils::task::{blocking_json_task, blocking_task};
@ -1242,16 +1243,35 @@ pub fn serve<T: BeaconChainTypes>(
)) ))
})) }))
.and(warp::path::end()) .and(warp::path::end())
.and(warp::header::optional::<api_types::Accept>("accept"))
.and(chain_filter.clone()) .and(chain_filter.clone())
.and_then(|state_id: StateId, chain: Arc<BeaconChain<T>>| { .and_then(
blocking_task(move || { |state_id: StateId,
state_id.map_state(&chain, |state| { accept_header: Option<api_types::Accept>,
Ok(warp::reply::json(&api_types::GenericResponseRef::from( chain: Arc<BeaconChain<T>>| {
&state, 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 // GET debug/beacon/heads
let get_debug_beacon_heads = eth1_v1 let get_debug_beacon_heads = eth1_v1

View File

@ -1374,7 +1374,12 @@ impl ApiTester {
pub async fn test_get_debug_beacon_states(self) -> Self { pub async fn test_get_debug_beacon_states(self) -> Self {
for state_id in self.interesting_state_ids() { 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 .client
.get_debug_beacon_states(state_id) .get_debug_beacon_states(state_id)
.await .await
@ -1384,7 +1389,8 @@ impl ApiTester {
let mut expected = self.get_state(state_id); let mut expected = self.get_state(state_id);
expected.as_mut().map(|state| state.drop_all_caches()); 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 self

View File

@ -20,6 +20,7 @@ pub use reqwest;
use reqwest::{IntoUrl, Response}; use reqwest::{IntoUrl, Response};
pub use reqwest::{StatusCode, Url}; pub use reqwest::{StatusCode, Url};
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use ssz::Decode;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fmt; use std::fmt;
use std::iter::Iterator; 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<U: IntoUrl>(
&self,
url: U,
accept_header: Accept,
) -> Result<Option<Vec<u8>>, 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::<Vec<_>>(),
)),
Err(err) => {
if err.status() == Some(StatusCode::NOT_FOUND) {
Ok(None)
} else {
Err(err)
}
}
}
}
/// Perform a HTTP POST request. /// Perform a HTTP POST request.
async fn post<T: Serialize, U: IntoUrl>(&self, url: U, body: &T) -> Result<(), Error> { async fn post<T: Serialize, U: IntoUrl>(&self, url: U, body: &T) -> Result<(), Error> {
let response = self let response = self
@ -824,6 +856,27 @@ impl BeaconNodeHttpClient {
self.get_opt(path).await self.get_opt(path).await
} }
/// `GET debug/beacon/states/{state_id}`
/// `-H "accept: application/octet-stream"`
pub async fn get_debug_beacon_states_ssz<T: EthSpec>(
&self,
state_id: StateId,
) -> Result<Option<BeaconState<T>>, 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` /// `GET debug/beacon/heads`
pub async fn get_debug_beacon_heads( pub async fn get_debug_beacon_heads(
&self, &self,

View File

@ -3,6 +3,7 @@
use crate::Error as ServerError; use crate::Error as ServerError;
use eth2_libp2p::{ConnectionDirection, Enr, Multiaddr, PeerConnectionStatus}; use eth2_libp2p::{ConnectionDirection, Enr, Multiaddr, PeerConnectionStatus};
pub use reqwest::header::ACCEPT;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fmt; 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<Self, Self::Err> {
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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;