diff --git a/beacon_node/http_api/src/block_id.rs b/beacon_node/http_api/src/block_id.rs index 4b7c6a879ed..4ceb20efa59 100644 --- a/beacon_node/http_api/src/block_id.rs +++ b/beacon_node/http_api/src/block_id.rs @@ -1,12 +1,20 @@ use beacon_chain::{BeaconChain, BeaconChainTypes}; use eth2::types::BlockId as CoreBlockId; use std::str::FromStr; -use types::{Hash256, SignedBeaconBlock}; +use types::{Hash256, SignedBeaconBlock, Slot}; #[derive(Debug)] pub struct BlockId(pub CoreBlockId); impl BlockId { + pub fn from_slot(slot: Slot) -> Self { + Self(CoreBlockId::Slot(slot)) + } + + pub fn from_root(root: Hash256) -> Self { + Self(CoreBlockId::Root(root)) + } + pub fn root( &self, chain: &BeaconChain, @@ -28,7 +36,11 @@ impl BlockId { CoreBlockId::Slot(slot) => chain .block_root_at_slot(*slot) .map_err(crate::reject::beacon_chain_error) - .and_then(|root_opt| root_opt.ok_or_else(|| warp::reject::not_found())), + .and_then(|root_opt| { + root_opt.ok_or_else(|| { + crate::reject::custom_not_found(format!("beacon block at slot {}", slot)) + }) + }), CoreBlockId::Root(root) => Ok(*root), } } @@ -41,10 +53,20 @@ impl BlockId { CoreBlockId::Head => chain .head_beacon_block() .map_err(crate::reject::beacon_chain_error), - _ => chain - .get_block(&self.root(chain)?) - .map_err(crate::reject::beacon_chain_error) - .and_then(|root_opt| root_opt.ok_or_else(|| warp::reject::not_found())), + _ => { + let root = self.root(chain)?; + chain + .get_block(&root) + .map_err(crate::reject::beacon_chain_error) + .and_then(|root_opt| { + root_opt.ok_or_else(|| { + crate::reject::custom_not_found(format!( + "beacon block with root {}", + root + )) + }) + }) + } } } } diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index c070527ce70..f8e88974926 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -5,14 +5,14 @@ mod state_id; use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; use block_id::BlockId; use eth2::types::{self as api_types, ValidatorId}; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use state_id::StateId; use std::borrow::Cow; use std::future::Future; use std::net::SocketAddr; use std::sync::Arc; use tokio::sync::oneshot; -use types::{CommitteeCache, Epoch, EthSpec, RelativeEpoch, Slot}; +use types::{CommitteeCache, Epoch, EthSpec, RelativeEpoch}; use warp::Filter; const API_PREFIX: &str = "eth"; @@ -194,24 +194,18 @@ pub fn serve( }, ); - #[derive(Serialize, Deserialize)] - struct CommitteesQuery { - slot: Option, - index: Option, - } - // beacon/states/{state_id}/committees/{epoch} let beacon_state_committees = beacon_states_path .clone() .and(warp::path("committees")) .and(warp::path::param::()) - .and(warp::query::()) + .and(warp::query::()) .and(warp::path::end()) .and_then( |state_id: StateId, chain: Arc>, epoch: Epoch, - query: CommitteesQuery| { + query: api_types::CommitteesQuery| { blocking_json_task(move || { state_id.map_state(&chain, |state| { let relative_epoch = @@ -284,8 +278,81 @@ pub fn serve( }, ); + // beacon/headers + // + // Note: this endpoint only returns information about blocks in the canonical chain. Given that + // there's a `canonical` flag on the response, I assume it should also return non-canonical + // things. Returning non-canonical things is hard for us since we don't already have a + // mechanism for arbitrary forwards block iteration, we only support iterating forwards along + // the canonical chain. + let beacon_headers = base_path + .and(warp::path("beacon")) + .and(warp::path("headers")) + .and(warp::query::()) + .and(chain_filter.clone()) + .and_then( + |query: api_types::HeadersQuery, chain: Arc>| { + blocking_json_task(move || { + let (root, block) = match (query.slot, query.parent_root) { + (None, None) => chain + .head_beacon_block() + .map_err(crate::reject::beacon_chain_error) + .map(|block| (block.canonical_root(), block))?, + (None, Some(parent_root)) => { + let parent = BlockId::from_root(parent_root).block(&chain)?; + let root = chain + .forwards_iter_block_roots(parent.slot()) + .map_err(crate::reject::beacon_chain_error)? + .next() + .transpose() + .map_err(crate::reject::beacon_chain_error)? + .map(|(root, _)| root) + .ok_or_else(|| { + crate::reject::custom_not_found(format!( + "child of block with root {}", + parent_root + )) + })?; + + BlockId::from_root(root) + .block(&chain) + .map(|block| (root, block))? + } + (Some(slot), parent_root_opt) => { + let root = BlockId::from_slot(slot).root(&chain)?; + let block = BlockId::from_root(root).block(&chain)?; + + // If the parent root was supplied, check that it matches the block + // obtained via a slot lookup. + if let Some(parent_root) = parent_root_opt { + if block.parent_root() != parent_root { + return Err(crate::reject::custom_not_found(format!( + "no canonical block at slot {} with parent root {}", + slot, parent_root + ))); + } + } + + (root, block) + } + }; + + let data = api_types::BlockHeaderData { + root, + canonical: true, + header: api_types::BlockHeaderAndSignature { + message: block.message.block_header(), + signature: block.signature.into(), + }, + }; + + Ok(api_types::GenericResponse::from(vec![data])) + }) + }, + ); + /* - * beacon/blocks + * beacon/blocks/{block_id} */ let beacon_blocks_path = base_path @@ -314,8 +381,9 @@ pub fn serve( .or(beacon_state_finality_checkpoints) .or(beacon_state_validators) .or(beacon_state_validators_id) - .or(beacon_block_root) .or(beacon_state_committees) + .or(beacon_headers) + .or(beacon_block_root) .recover(crate::reject::handle_rejection); let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>(); diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index e81a4530d38..03cfc6cc52e 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -449,6 +449,41 @@ impl ApiTester { } } + pub async fn test_beacon_headers_all_slots(self) -> Self { + for slot in 0..CHAIN_LENGTH { + let slot = Slot::from(slot); + + let result = self + .client + .beacon_headers(Some(slot), None) + .await + .unwrap() + .map(|res| res.data); + + let root = self.chain.block_root_at_slot(slot).unwrap(); + + if root.is_none() && result.is_none() { + continue; + } + + let root = root.unwrap(); + let block = self.chain.block_at_slot(slot).unwrap().unwrap(); + let header = BlockHeaderData { + root, + canonical: true, + header: BlockHeaderAndSignature { + message: block.message.block_header(), + signature: block.signature.into(), + }, + }; + let expected = vec![header]; + + assert_eq!(result.unwrap(), expected, "slot {:?}", slot); + } + + self + } + pub async fn test_beacon_blocks_root(self) -> Self { for block_id in self.interesting_block_ids() { let result = self @@ -504,6 +539,11 @@ async fn beacon_states_validator_id() { ApiTester::new().test_beacon_states_validator_id().await; } +#[tokio::test(core_threads = 2)] +async fn beacon_headers() { + ApiTester::new().test_beacon_headers_all_slots().await; +} + #[tokio::test(core_threads = 2)] async fn beacon_blocks_root() { ApiTester::new().test_beacon_blocks_root().await; diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 08eafdff572..54376cf435d 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -191,6 +191,34 @@ impl BeaconNodeClient { self.get_opt(path).await } + /// `GET beacon/headers?slot,parent_root` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn beacon_headers( + &self, + slot: Option, + parent_root: Option, + ) -> Result>>, Error> { + let mut path = self.server.clone(); + + path.path_segments_mut() + .expect("path is base") + .push("beacon") + .push("headers"); + + if let Some(slot) = slot { + path.query_pairs_mut() + .append_pair("slot", &slot.to_string()); + } + + if let Some(root) = parent_root { + path.query_pairs_mut() + .append_pair("parent_root", &root.to_string()); + } + + self.get_opt(path).await + } + /// `GET beacon/blocks/{block_id}/root` /// /// Returns `Ok(None)` on a 404 error. diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 7bd31c81965..1531a1c8c1f 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -3,7 +3,10 @@ use std::fmt; use std::str::FromStr; use types::serde_utils; -pub use types::{Checkpoint, Epoch, Fork, Hash256, PublicKeyBytes, Slot, Validator}; +pub use types::{ + BeaconBlockHeader, Checkpoint, Epoch, Fork, Hash256, PublicKeyBytes, SignatureBytes, Slot, + Validator, +}; /// The number of epochs between when a validator is eligible for activation and when they /// *usually* enter the activation queue. @@ -238,6 +241,12 @@ impl ValidatorStatus { } } +#[derive(Serialize, Deserialize)] +pub struct CommitteesQuery { + pub slot: Option, + pub index: Option, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct CommitteeData { #[serde(with = "serde_utils::quoted")] @@ -246,3 +255,22 @@ pub struct CommitteeData { #[serde(with = "serde_utils::quoted_u64_vec")] pub validators: Vec, } + +#[derive(Serialize, Deserialize)] +pub struct HeadersQuery { + pub slot: Option, + pub parent_root: Option, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BlockHeaderAndSignature { + pub message: BeaconBlockHeader, + pub signature: SignatureBytes, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct BlockHeaderData { + pub root: Hash256, + pub canonical: bool, + pub header: BlockHeaderAndSignature, +} diff --git a/consensus/types/src/beacon_block_header.rs b/consensus/types/src/beacon_block_header.rs index 04a20e56d3f..1ff785cf873 100644 --- a/consensus/types/src/beacon_block_header.rs +++ b/consensus/types/src/beacon_block_header.rs @@ -14,6 +14,7 @@ use tree_hash_derive::TreeHash; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Encode, Decode, TreeHash, TestRandom)] pub struct BeaconBlockHeader { pub slot: Slot, + #[serde(with = "crate::serde_utils::quoted")] pub proposer_index: u64, pub parent_root: Hash256, pub state_root: Hash256,