diff --git a/Cargo.lock b/Cargo.lock index a1c923f77..20cafb964 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1574,6 +1574,7 @@ dependencies = [ "bytes 0.5.6", "eth2_keystore", "eth2_libp2p", + "eth2_ssz", "hex 0.4.2", "libsecp256k1", "procinfo", @@ -2461,6 +2462,7 @@ dependencies = [ "eth1", "eth2", "eth2_libp2p", + "eth2_ssz", "fork_choice", "hex 0.4.2", "lazy_static", diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index 7b2cd83a7..acaea7399 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -24,6 +24,7 @@ lighthouse_metrics = { path = "../../common/lighthouse_metrics" } lazy_static = "1.4.0" warp_utils = { path = "../../common/warp_utils" } slot_clock = { path = "../../common/slot_clock" } +eth2_ssz = { path = "../../consensus/ssz" } bs58 = "0.3.1" [dev-dependencies] diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 487880a93..fd6adf02c 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -28,6 +28,7 @@ use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use slog::{crit, error, info, trace, warn, Logger}; use slot_clock::SlotClock; +use ssz::Encode; use state_id::StateId; use state_processing::per_slot_processing; use std::borrow::Cow; @@ -41,7 +42,7 @@ use types::{ Hash256, ProposerSlashing, PublicKey, RelativeEpoch, SignedAggregateAndProof, SignedBeaconBlock, SignedVoluntaryExit, Slot, YamlConfig, }; -use warp::Filter; +use warp::{http::Response, Filter}; use warp_utils::task::{blocking_json_task, blocking_task}; const API_PREFIX: &str = "eth"; @@ -1768,7 +1769,7 @@ pub fn serve( .and(warp::path::param::()) .and(warp::path("global")) .and(warp::path::end()) - .and(chain_filter) + .and(chain_filter.clone()) .and_then(|epoch: Epoch, chain: Arc>| { blocking_json_task(move || { validator_inclusion::global_validator_inclusion_data(epoch, &chain) @@ -1776,6 +1777,30 @@ pub fn serve( }) }); + // GET lighthouse/beacon/states/{state_id}/ssz + let get_lighthouse_beacon_states_ssz = warp::path("lighthouse") + .and(warp::path("beacon")) + .and(warp::path("states")) + .and(warp::path::param::()) + .and(warp::path("ssz")) + .and(warp::path::end()) + .and(chain_filter) + .and_then(|state_id: StateId, chain: Arc>| { + blocking_task(move || { + let state = state_id.state(&chain)?; + Response::builder() + .status(200) + .header("Content-Type", "application/ssz") + .body(state.as_ssz_bytes()) + .map_err(|e| { + warp_utils::reject::custom_server_error(format!( + "failed to create response: {}", + e + )) + }) + }) + }); + // Define the ultimate set of routes that will be provided to the server. let routes = warp::get() .and( @@ -1818,6 +1843,7 @@ pub fn serve( .or(get_lighthouse_proto_array.boxed()) .or(get_lighthouse_validator_inclusion_global.boxed()) .or(get_lighthouse_validator_inclusion.boxed()) + .or(get_lighthouse_beacon_states_ssz.boxed()) .boxed(), ) .or(warp::post() diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index adcd0ae5e..0b5154ce2 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -1571,6 +1571,23 @@ impl ApiTester { self } + + pub async fn test_get_lighthouse_beacon_states_ssz(self) -> Self { + for state_id in self.interesting_state_ids() { + let result = self + .client + .get_lighthouse_beacon_states_ssz(&state_id) + .await + .unwrap(); + + let mut expected = self.get_state(state_id); + expected.as_mut().map(|state| state.drop_all_caches()); + + assert_eq!(result, expected, "{:?}", state_id); + } + + self + } } #[tokio::test(core_threads = 2)] @@ -1871,5 +1888,7 @@ async fn lighthouse_endpoints() { .test_get_lighthouse_validator_inclusion() .await .test_get_lighthouse_validator_inclusion_global() + .await + .test_get_lighthouse_beacon_states_ssz() .await; } diff --git a/book/src/api-lighthouse.md b/book/src/api-lighthouse.md index 3f37673fa..2db412f75 100644 --- a/book/src/api-lighthouse.md +++ b/book/src/api-lighthouse.md @@ -177,3 +177,17 @@ See [Validator Inclusion APIs](./validator-inclusion.md). ### `/lighthouse/validator_inclusion/{epoch}/global` See [Validator Inclusion APIs](./validator-inclusion.md). + +### `/lighthouse/beacon/states/{state_id}/ssz` + +Obtains a `BeaconState` in SSZ bytes. Useful for obtaining a genesis state. + +The `state_id` parameter is identical to that used in the [Standard Eth2.0 API +`beacon/state` +routes](https://ethereum.github.io/eth2.0-APIs/#/Beacon/getStateRoot). + +```bash +curl -X GET "http://localhost:5052/lighthouse/beacon/states/0/ssz" | jq +``` + +*Example omitted for brevity, the body simply contains SSZ bytes.* diff --git a/common/eth2/Cargo.toml b/common/eth2/Cargo.toml index 0c1528e9b..b89c5d7c0 100644 --- a/common/eth2/Cargo.toml +++ b/common/eth2/Cargo.toml @@ -21,6 +21,7 @@ libsecp256k1 = "0.3.5" ring = "0.16.12" bytes = "0.5.6" account_utils = { path = "../../common/account_utils" } +eth2_ssz = { path = "../../consensus/ssz" } [target.'cfg(target_os = "linux")'.dependencies] psutil = { version = "3.2.0", optional = true } diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 9c1019b55..8a0ffbe35 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -40,6 +40,8 @@ pub enum Error { MissingSignatureHeader, /// The server returned an invalid JSON response. InvalidJson(serde_json::Error), + /// The server returned an invalid SSZ response. + InvalidSsz(ssz::DecodeError), } impl Error { @@ -54,6 +56,7 @@ impl Error { Error::InvalidSignatureHeader => None, Error::MissingSignatureHeader => None, Error::InvalidJson(_) => None, + Error::InvalidSsz(_) => None, } } } diff --git a/common/eth2/src/lighthouse.rs b/common/eth2/src/lighthouse.rs index 8bfbad84e..9735c7505 100644 --- a/common/eth2/src/lighthouse.rs +++ b/common/eth2/src/lighthouse.rs @@ -1,11 +1,14 @@ //! This module contains endpoints that are non-standard and only available on Lighthouse servers. use crate::{ - types::{Epoch, EthSpec, GenericResponse, ValidatorId}, - BeaconNodeHttpClient, Error, + ok_or_error, + types::{BeaconState, Epoch, EthSpec, GenericResponse, ValidatorId}, + BeaconNodeHttpClient, Error, StateId, StatusCode, }; use proto_array::core::ProtoArray; +use reqwest::IntoUrl; use serde::{Deserialize, Serialize}; +use ssz::Decode; pub use eth2_libp2p::{types::SyncState, PeerInfo}; @@ -143,6 +146,27 @@ impl Health { } impl BeaconNodeHttpClient { + /// Perform a HTTP GET request, returning `None` on a 404 error. + async fn get_bytes_opt(&self, url: U) -> Result>, Error> { + let response = self.client.get(url).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) + } + } + } + } + /// `GET lighthouse/health` pub async fn get_lighthouse_health(&self) -> Result, Error> { let mut path = self.server.clone(); @@ -221,4 +245,25 @@ impl BeaconNodeHttpClient { self.get(path).await } + + /// `GET lighthouse/beacon/states/{state_id}/ssz` + pub async fn get_lighthouse_beacon_states_ssz( + &self, + state_id: &StateId, + ) -> Result>, Error> { + let mut path = self.server.clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("beacon") + .push("states") + .push(&state_id.to_string()) + .push("ssz"); + + self.get_bytes_opt(path) + .await? + .map(|bytes| BeaconState::from_ssz_bytes(&bytes).map_err(Error::InvalidSsz)) + .transpose() + } }