mirror of
https://github.com/sigp/lighthouse.git
synced 2026-04-19 13:58:28 +00:00
Add API version headers and map_fork_name! (#2745)
## Proposed Changes * Add the `Eth-Consensus-Version` header to the HTTP API for the block and state endpoints. This is part of the v2.1.0 API that was recently released: https://github.com/ethereum/beacon-APIs/pull/170 * Add tests for the above. I refactored the `eth2` crate's helper functions to make this more straight-forward, and introduced some new mixin traits that I think greatly improve readability and flexibility. * Add a new `map_with_fork!` macro which is useful for decoding a superstruct type without naming all its variants. It is now used for SSZ-decoding `BeaconBlock` and `BeaconState`, and for JSON-decoding `SignedBeaconBlock` in the API. ## Additional Info The `map_with_fork!` changes will conflict with the Merge changes, but when resolving the conflict the changes from this branch should be preferred (it is no longer necessary to enumerate every fork). The merge fork _will_ need to be added to `map_fork_name_with`.
This commit is contained in:
@@ -10,14 +10,17 @@
|
||||
#[cfg(feature = "lighthouse")]
|
||||
pub mod lighthouse;
|
||||
pub mod lighthouse_vc;
|
||||
pub mod mixin;
|
||||
pub mod types;
|
||||
|
||||
use self::mixin::{RequestAccept, ResponseForkName, ResponseOptional};
|
||||
use self::types::{Error as ResponseError, *};
|
||||
use ::types::map_fork_name_with;
|
||||
use futures::Stream;
|
||||
use futures_util::StreamExt;
|
||||
use lighthouse_network::PeerId;
|
||||
pub use reqwest;
|
||||
use reqwest::{IntoUrl, Response};
|
||||
use reqwest::{IntoUrl, RequestBuilder, Response};
|
||||
pub use reqwest::{StatusCode, Url};
|
||||
use sensitive_url::SensitiveUrl;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
@@ -29,6 +32,8 @@ use std::time::Duration;
|
||||
pub const V1: EndpointVersion = EndpointVersion(1);
|
||||
pub const V2: EndpointVersion = EndpointVersion(2);
|
||||
|
||||
pub const CONSENSUS_VERSION_HEADER: &str = "Eth-Consensus-Version";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// The `reqwest` client raised an error.
|
||||
@@ -55,6 +60,12 @@ pub enum Error {
|
||||
InvalidSsz(ssz::DecodeError),
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(error: reqwest::Error) -> Self {
|
||||
Error::Reqwest(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// If the error has a HTTP status code, return it.
|
||||
pub fn status(&self) -> Option<StatusCode> {
|
||||
@@ -161,12 +172,18 @@ impl BeaconNodeHttpClient {
|
||||
|
||||
/// Perform a HTTP GET request.
|
||||
async fn get<T: DeserializeOwned, U: IntoUrl>(&self, url: U) -> Result<T, Error> {
|
||||
let response = self.client.get(url).send().await.map_err(Error::Reqwest)?;
|
||||
ok_or_error(response)
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
.map_err(Error::Reqwest)
|
||||
let response = self.get_response(url, |b| b).await?;
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
|
||||
/// Perform an HTTP GET request, returning the `Response` for processing.
|
||||
pub async fn get_response<U: IntoUrl>(
|
||||
&self,
|
||||
url: U,
|
||||
builder: impl FnOnce(RequestBuilder) -> RequestBuilder,
|
||||
) -> Result<Response, Error> {
|
||||
let response = builder(self.client.get(url)).send().await?;
|
||||
ok_or_error(response).await
|
||||
}
|
||||
|
||||
/// Perform a HTTP GET request with a custom timeout.
|
||||
@@ -176,31 +193,16 @@ impl BeaconNodeHttpClient {
|
||||
timeout: Duration,
|
||||
) -> Result<T, Error> {
|
||||
let response = self
|
||||
.client
|
||||
.get(url)
|
||||
.timeout(timeout)
|
||||
.send()
|
||||
.await
|
||||
.map_err(Error::Reqwest)?;
|
||||
ok_or_error(response)
|
||||
.await?
|
||||
.json()
|
||||
.await
|
||||
.map_err(Error::Reqwest)
|
||||
.get_response(url, |builder| builder.timeout(timeout))
|
||||
.await?;
|
||||
Ok(response.json().await?)
|
||||
}
|
||||
|
||||
/// Perform a HTTP GET request, returning `None` on a 404 error.
|
||||
async fn get_opt<T: DeserializeOwned, U: IntoUrl>(&self, url: U) -> Result<Option<T>, Error> {
|
||||
let response = self.client.get(url).send().await.map_err(Error::Reqwest)?;
|
||||
match ok_or_error(response).await {
|
||||
Ok(resp) => resp.json().await.map(Option::Some).map_err(Error::Reqwest),
|
||||
Err(err) => {
|
||||
if err.status() == Some(StatusCode::NOT_FOUND) {
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
match self.get_response(url, |b| b).await.optional()? {
|
||||
Some(response) => Ok(Some(response.json().await?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,22 +212,13 @@ impl BeaconNodeHttpClient {
|
||||
url: U,
|
||||
timeout: Duration,
|
||||
) -> Result<Option<T>, Error> {
|
||||
let response = self
|
||||
.client
|
||||
.get(url)
|
||||
.timeout(timeout)
|
||||
.send()
|
||||
let opt_response = self
|
||||
.get_response(url, |b| b.timeout(timeout))
|
||||
.await
|
||||
.map_err(Error::Reqwest)?;
|
||||
match ok_or_error(response).await {
|
||||
Ok(resp) => resp.json().await.map(Option::Some).map_err(Error::Reqwest),
|
||||
Err(err) => {
|
||||
if err.status() == Some(StatusCode::NOT_FOUND) {
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
.optional()?;
|
||||
match opt_response {
|
||||
Some(response) => Ok(Some(response.json().await?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,28 +228,13 @@ impl BeaconNodeHttpClient {
|
||||
url: U,
|
||||
accept_header: Accept,
|
||||
) -> Result<Option<Vec<u8>>, Error> {
|
||||
let response = self
|
||||
.client
|
||||
.get(url)
|
||||
.header(ACCEPT, accept_header.to_string())
|
||||
.send()
|
||||
let opt_response = self
|
||||
.get_response(url, |b| b.accept(accept_header))
|
||||
.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)
|
||||
}
|
||||
}
|
||||
.optional()?;
|
||||
match opt_response {
|
||||
Some(resp) => Ok(Some(resp.bytes().await?.into_iter().collect::<Vec<_>>())),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,7 +293,7 @@ impl BeaconNodeHttpClient {
|
||||
if let Some(timeout) = timeout {
|
||||
builder = builder.timeout(timeout);
|
||||
}
|
||||
let response = builder.json(body).send().await.map_err(Error::Reqwest)?;
|
||||
let response = builder.json(body).send().await?;
|
||||
ok_or_error(response).await
|
||||
}
|
||||
|
||||
@@ -607,6 +585,17 @@ impl BeaconNodeHttpClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Path for `v2/beacon/blocks`
|
||||
pub fn get_beacon_blocks_path(&self, block_id: BlockId) -> Result<Url, Error> {
|
||||
let mut path = self.eth_path(V2)?;
|
||||
path.path_segments_mut()
|
||||
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
||||
.push("beacon")
|
||||
.push("blocks")
|
||||
.push(&block_id.to_string());
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// `GET v2/beacon/blocks`
|
||||
///
|
||||
/// Returns `Ok(None)` on a 404 error.
|
||||
@@ -614,15 +603,30 @@ impl BeaconNodeHttpClient {
|
||||
&self,
|
||||
block_id: BlockId,
|
||||
) -> Result<Option<ForkVersionedResponse<SignedBeaconBlock<T>>>, Error> {
|
||||
let mut path = self.eth_path(V2)?;
|
||||
let path = self.get_beacon_blocks_path(block_id)?;
|
||||
let response = match self.get_response(path, |b| b).await.optional()? {
|
||||
Some(res) => res,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
path.path_segments_mut()
|
||||
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
||||
.push("beacon")
|
||||
.push("blocks")
|
||||
.push(&block_id.to_string());
|
||||
|
||||
self.get_opt(path).await
|
||||
// If present, use the fork provided in the headers to decode the block. Gracefully handle
|
||||
// missing and malformed fork names by falling back to regular deserialisation.
|
||||
let (block, version) = match response.fork_name_from_header() {
|
||||
Ok(Some(fork_name)) => {
|
||||
map_fork_name_with!(fork_name, SignedBeaconBlock, {
|
||||
let ForkVersionedResponse { version, data } = response.json().await?;
|
||||
(data, version)
|
||||
})
|
||||
}
|
||||
Ok(None) | Err(_) => {
|
||||
let ForkVersionedResponse { version, data } = response.json().await?;
|
||||
(data, version)
|
||||
}
|
||||
};
|
||||
Ok(Some(ForkVersionedResponse {
|
||||
version,
|
||||
data: block,
|
||||
}))
|
||||
}
|
||||
|
||||
/// `GET v1/beacon/blocks` (LEGACY)
|
||||
@@ -651,13 +655,7 @@ impl BeaconNodeHttpClient {
|
||||
block_id: BlockId,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Option<SignedBeaconBlock<T>>, Error> {
|
||||
let mut path = self.eth_path(V2)?;
|
||||
|
||||
path.path_segments_mut()
|
||||
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
||||
.push("beacon")
|
||||
.push("blocks")
|
||||
.push(&block_id.to_string());
|
||||
let path = self.get_beacon_blocks_path(block_id)?;
|
||||
|
||||
self.get_bytes_opt_accept_header(path, Accept::Ssz)
|
||||
.await?
|
||||
@@ -966,13 +964,7 @@ impl BeaconNodeHttpClient {
|
||||
.push("node")
|
||||
.push("health");
|
||||
|
||||
let status = self
|
||||
.client
|
||||
.get(path)
|
||||
.send()
|
||||
.await
|
||||
.map_err(Error::Reqwest)?
|
||||
.status();
|
||||
let status = self.client.get(path).send().await?.status();
|
||||
if status == StatusCode::OK || status == StatusCode::PARTIAL_CONTENT {
|
||||
Ok(status)
|
||||
} else {
|
||||
@@ -1042,11 +1034,8 @@ impl BeaconNodeHttpClient {
|
||||
self.get(path).await
|
||||
}
|
||||
|
||||
/// `GET v2/debug/beacon/states/{state_id}`
|
||||
pub async fn get_debug_beacon_states<T: EthSpec>(
|
||||
&self,
|
||||
state_id: StateId,
|
||||
) -> Result<Option<ForkVersionedResponse<BeaconState<T>>>, Error> {
|
||||
/// URL path for `v2/debug/beacon/states/{state_id}`.
|
||||
pub fn get_debug_beacon_states_path(&self, state_id: StateId) -> Result<Url, Error> {
|
||||
let mut path = self.eth_path(V2)?;
|
||||
|
||||
path.path_segments_mut()
|
||||
@@ -1055,7 +1044,15 @@ impl BeaconNodeHttpClient {
|
||||
.push("beacon")
|
||||
.push("states")
|
||||
.push(&state_id.to_string());
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// `GET v2/debug/beacon/states/{state_id}`
|
||||
pub async fn get_debug_beacon_states<T: EthSpec>(
|
||||
&self,
|
||||
state_id: StateId,
|
||||
) -> Result<Option<ForkVersionedResponse<BeaconState<T>>>, Error> {
|
||||
let path = self.get_debug_beacon_states_path(state_id)?;
|
||||
self.get_opt(path).await
|
||||
}
|
||||
|
||||
@@ -1083,14 +1080,7 @@ impl BeaconNodeHttpClient {
|
||||
state_id: StateId,
|
||||
spec: &ChainSpec,
|
||||
) -> Result<Option<BeaconState<T>>, Error> {
|
||||
let mut path = self.eth_path(V1)?;
|
||||
|
||||
path.path_segments_mut()
|
||||
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
||||
.push("debug")
|
||||
.push("beacon")
|
||||
.push("states")
|
||||
.push(&state_id.to_string());
|
||||
let path = self.get_debug_beacon_states_path(state_id)?;
|
||||
|
||||
self.get_bytes_opt_accept_header(path, Accept::Ssz)
|
||||
.await?
|
||||
@@ -1343,8 +1333,7 @@ impl BeaconNodeHttpClient {
|
||||
.client
|
||||
.get(path)
|
||||
.send()
|
||||
.await
|
||||
.map_err(Error::Reqwest)?
|
||||
.await?
|
||||
.bytes_stream()
|
||||
.map(|next| match next {
|
||||
Ok(bytes) => EventKind::from_sse_bytes(bytes.as_ref()),
|
||||
|
||||
50
common/eth2/src/mixin.rs
Normal file
50
common/eth2/src/mixin.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use crate::{types::Accept, Error, CONSENSUS_VERSION_HEADER};
|
||||
use reqwest::{header::ACCEPT, RequestBuilder, Response, StatusCode};
|
||||
use std::str::FromStr;
|
||||
use types::ForkName;
|
||||
|
||||
/// Trait for converting a 404 error into an `Option<Response>`.
|
||||
pub trait ResponseOptional {
|
||||
fn optional(self) -> Result<Option<Response>, Error>;
|
||||
}
|
||||
|
||||
impl ResponseOptional for Result<Response, Error> {
|
||||
fn optional(self) -> Result<Option<Response>, Error> {
|
||||
match self {
|
||||
Ok(x) => Ok(Some(x)),
|
||||
Err(e) if e.status() == Some(StatusCode::NOT_FOUND) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for extracting the fork name from the headers of a response.
|
||||
pub trait ResponseForkName {
|
||||
#[allow(clippy::result_unit_err)]
|
||||
fn fork_name_from_header(&self) -> Result<Option<ForkName>, ()>;
|
||||
}
|
||||
|
||||
impl ResponseForkName for Response {
|
||||
fn fork_name_from_header(&self) -> Result<Option<ForkName>, ()> {
|
||||
self.headers()
|
||||
.get(CONSENSUS_VERSION_HEADER)
|
||||
.map(|fork_name| {
|
||||
fork_name
|
||||
.to_str()
|
||||
.map_err(|_| ())
|
||||
.and_then(ForkName::from_str)
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for adding an "accept" header to a request builder.
|
||||
pub trait RequestAccept {
|
||||
fn accept(self, accept: Accept) -> RequestBuilder;
|
||||
}
|
||||
|
||||
impl RequestAccept for RequestBuilder {
|
||||
fn accept(self, accept: Accept) -> RequestBuilder {
|
||||
self.header(ACCEPT, accept.to_string())
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
use crate::Error as ServerError;
|
||||
use lighthouse_network::{ConnectionDirection, Enr, Multiaddr, PeerConnectionStatus};
|
||||
pub use reqwest::header::ACCEPT;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
@@ -213,7 +212,6 @@ impl<'a, T: Serialize> From<&'a T> for GenericResponseRef<'a, T> {
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
// #[serde(bound = "T: Serialize + serde::de::DeserializeOwned")]
|
||||
pub struct ForkVersionedResponse<T> {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub version: Option<ForkName>,
|
||||
|
||||
Reference in New Issue
Block a user