Blinded block and RANDAO APIs (#3571)

## Issue Addressed

https://github.com/ethereum/beacon-APIs/pull/241
https://github.com/ethereum/beacon-APIs/pull/242

## Proposed Changes

Implement two new endpoints for fetching blinded blocks and RANDAO mixes.


Co-authored-by: realbigsean <sean@sigmaprime.io>
This commit is contained in:
Michael Sproul 2022-11-11 00:38:27 +00:00
parent 266d765285
commit d99bfcf1a5
4 changed files with 296 additions and 0 deletions

View File

@ -891,6 +891,37 @@ pub fn serve<T: BeaconChainTypes>(
}, },
); );
// GET beacon/states/{state_id}/randao?epoch
let get_beacon_state_randao = beacon_states_path
.clone()
.and(warp::path("randao"))
.and(warp::query::<api_types::RandaoQuery>())
.and(warp::path::end())
.and_then(
|state_id: StateId, chain: Arc<BeaconChain<T>>, query: api_types::RandaoQuery| {
blocking_json_task(move || {
let (randao, execution_optimistic) = state_id
.map_state_and_execution_optimistic(
&chain,
|state, execution_optimistic| {
let epoch = query.epoch.unwrap_or_else(|| state.current_epoch());
let randao = *state.get_randao_mix(epoch).map_err(|e| {
warp_utils::reject::custom_bad_request(format!(
"epoch out of range: {e:?}"
))
})?;
Ok((randao, execution_optimistic))
},
)?;
Ok(
api_types::GenericResponse::from(api_types::RandaoMix { randao })
.add_execution_optimistic(execution_optimistic),
)
})
},
);
// GET beacon/headers // GET beacon/headers
// //
// Note: this endpoint only returns information about blocks in the canonical chain. Given that // Note: this endpoint only returns information about blocks in the canonical chain. Given that
@ -1167,6 +1198,51 @@ pub fn serve<T: BeaconChainTypes>(
}) })
}); });
// GET beacon/blinded_blocks/{block_id}
let get_beacon_blinded_block = eth_v1
.and(warp::path("beacon"))
.and(warp::path("blinded_blocks"))
.and(block_id_or_err)
.and(chain_filter.clone())
.and(warp::path::end())
.and(warp::header::optional::<api_types::Accept>("accept"))
.and_then(
|block_id: BlockId,
chain: Arc<BeaconChain<T>>,
accept_header: Option<api_types::Accept>| {
blocking_task(move || {
let (block, execution_optimistic) = block_id.blinded_block(&chain)?;
let fork_name = block
.fork_name(&chain.spec)
.map_err(inconsistent_fork_rejection)?;
match accept_header {
Some(api_types::Accept::Ssz) => Response::builder()
.status(200)
.header("Content-Type", "application/octet-stream")
.body(block.as_ssz_bytes().into())
.map_err(|e| {
warp_utils::reject::custom_server_error(format!(
"failed to create response: {}",
e
))
}),
_ => {
// Post as a V2 endpoint so we return the fork version.
execution_optimistic_fork_versioned_response(
V2,
fork_name,
execution_optimistic,
block,
)
.map(|res| warp::reply::json(&res).into_response())
}
}
.map(|resp| add_consensus_version_header(resp, fork_name))
})
},
);
/* /*
* beacon/pool * beacon/pool
*/ */
@ -3164,10 +3240,12 @@ pub fn serve<T: BeaconChainTypes>(
.or(get_beacon_state_validators.boxed()) .or(get_beacon_state_validators.boxed())
.or(get_beacon_state_committees.boxed()) .or(get_beacon_state_committees.boxed())
.or(get_beacon_state_sync_committees.boxed()) .or(get_beacon_state_sync_committees.boxed())
.or(get_beacon_state_randao.boxed())
.or(get_beacon_headers.boxed()) .or(get_beacon_headers.boxed())
.or(get_beacon_headers_block_id.boxed()) .or(get_beacon_headers_block_id.boxed())
.or(get_beacon_block.boxed()) .or(get_beacon_block.boxed())
.or(get_beacon_block_attestations.boxed()) .or(get_beacon_block_attestations.boxed())
.or(get_beacon_blinded_block.boxed())
.or(get_beacon_block_root.boxed()) .or(get_beacon_block_root.boxed())
.or(get_beacon_pool_attestations.boxed()) .or(get_beacon_pool_attestations.boxed())
.or(get_beacon_pool_attester_slashings.boxed()) .or(get_beacon_pool_attester_slashings.boxed())
@ -3212,6 +3290,7 @@ pub fn serve<T: BeaconChainTypes>(
.or(get_lighthouse_merge_readiness.boxed()) .or(get_lighthouse_merge_readiness.boxed())
.or(get_events.boxed()), .or(get_events.boxed()),
) )
.boxed()
.or(warp::post().and( .or(warp::post().and(
post_beacon_blocks post_beacon_blocks
.boxed() .boxed()

View File

@ -745,6 +745,36 @@ impl ApiTester {
self self
} }
pub async fn test_beacon_states_randao(self) -> Self {
for state_id in self.interesting_state_ids() {
let mut state_opt = state_id
.state(&self.chain)
.ok()
.map(|(state, _execution_optimistic)| state);
let epoch_opt = state_opt.as_ref().map(|state| state.current_epoch());
let result = self
.client
.get_beacon_states_randao(state_id.0, epoch_opt)
.await
.unwrap()
.map(|res| res.data);
if result.is_none() && state_opt.is_none() {
continue;
}
let state = state_opt.as_mut().expect("result should be none");
let randao_mix = state
.get_randao_mix(state.slot().epoch(E::slots_per_epoch()))
.unwrap();
assert_eq!(result.unwrap().randao, *randao_mix);
}
self
}
pub async fn test_beacon_headers_all_slots(self) -> Self { pub async fn test_beacon_headers_all_slots(self) -> Self {
for slot in 0..CHAIN_LENGTH { for slot in 0..CHAIN_LENGTH {
let slot = Slot::from(slot); let slot = Slot::from(slot);
@ -1016,6 +1046,82 @@ impl ApiTester {
self self
} }
pub async fn test_beacon_blinded_blocks(self) -> Self {
for block_id in self.interesting_block_ids() {
let expected = block_id
.blinded_block(&self.chain)
.ok()
.map(|(block, _execution_optimistic)| block);
if let CoreBlockId::Slot(slot) = block_id.0 {
if expected.is_none() {
assert!(SKIPPED_SLOTS.contains(&slot.as_u64()));
} else {
assert!(!SKIPPED_SLOTS.contains(&slot.as_u64()));
}
}
// Check the JSON endpoint.
let json_result = self
.client
.get_beacon_blinded_blocks(block_id.0)
.await
.unwrap();
if let (Some(json), Some(expected)) = (&json_result, &expected) {
assert_eq!(&json.data, expected, "{:?}", block_id);
assert_eq!(
json.version,
Some(expected.fork_name(&self.chain.spec).unwrap())
);
} else {
assert_eq!(json_result, None);
assert_eq!(expected, None);
}
// Check the SSZ endpoint.
let ssz_result = self
.client
.get_beacon_blinded_blocks_ssz(block_id.0, &self.chain.spec)
.await
.unwrap();
assert_eq!(ssz_result.as_ref(), expected.as_ref(), "{:?}", block_id);
// Check that version headers are provided.
let url = self
.client
.get_beacon_blinded_blocks_path(block_id.0)
.unwrap();
let builders: Vec<fn(RequestBuilder) -> RequestBuilder> = vec![
|b| b,
|b| b.accept(Accept::Ssz),
|b| b.accept(Accept::Json),
|b| b.accept(Accept::Any),
];
for req_builder in builders {
let raw_res = self
.client
.get_response(url.clone(), req_builder)
.await
.optional()
.unwrap();
if let (Some(raw_res), Some(expected)) = (&raw_res, &expected) {
assert_eq!(
raw_res.fork_name_from_header().unwrap(),
Some(expected.fork_name(&self.chain.spec).unwrap())
);
} else {
assert!(raw_res.is_none());
assert_eq!(expected, None);
}
}
}
self
}
pub async fn test_beacon_blocks_attestations(self) -> Self { pub async fn test_beacon_blocks_attestations(self) -> Self {
for block_id in self.interesting_block_ids() { for block_id in self.interesting_block_ids() {
let result = self let result = self
@ -3696,6 +3802,8 @@ async fn beacon_get() {
.await .await
.test_beacon_states_validator_id() .test_beacon_states_validator_id()
.await .await
.test_beacon_states_randao()
.await
.test_beacon_headers_all_slots() .test_beacon_headers_all_slots()
.await .await
.test_beacon_headers_all_parents() .test_beacon_headers_all_parents()
@ -3704,6 +3812,8 @@ async fn beacon_get() {
.await .await
.test_beacon_blocks() .test_beacon_blocks()
.await .await
.test_beacon_blinded_blocks()
.await
.test_beacon_blocks_attestations() .test_beacon_blocks_attestations()
.await .await
.test_beacon_blocks_root() .test_beacon_blocks_root()

View File

@ -518,6 +518,29 @@ impl BeaconNodeHttpClient {
self.get(path).await self.get(path).await
} }
/// `GET beacon/states/{state_id}/randao?epoch`
pub async fn get_beacon_states_randao(
&self,
state_id: StateId,
epoch: Option<Epoch>,
) -> Result<Option<ExecutionOptimisticResponse<RandaoMix>>, Error> {
let mut path = self.eth_path(V1)?;
path.path_segments_mut()
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
.push("beacon")
.push("states")
.push(&state_id.to_string())
.push("randao");
if let Some(epoch) = epoch {
path.query_pairs_mut()
.append_pair("epoch", &epoch.to_string());
}
self.get_opt(path).await
}
/// `GET beacon/states/{state_id}/validators/{validator_id}` /// `GET beacon/states/{state_id}/validators/{validator_id}`
/// ///
/// Returns `Ok(None)` on a 404 error. /// Returns `Ok(None)` on a 404 error.
@ -636,6 +659,17 @@ impl BeaconNodeHttpClient {
Ok(path) Ok(path)
} }
/// Path for `v1/beacon/blinded_blocks/{block_id}`
pub fn get_beacon_blinded_blocks_path(&self, block_id: BlockId) -> Result<Url, Error> {
let mut path = self.eth_path(V1)?;
path.path_segments_mut()
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
.push("beacon")
.push("blinded_blocks")
.push(&block_id.to_string());
Ok(path)
}
/// `GET v2/beacon/blocks` /// `GET v2/beacon/blocks`
/// ///
/// Returns `Ok(None)` on a 404 error. /// Returns `Ok(None)` on a 404 error.
@ -680,6 +714,51 @@ impl BeaconNodeHttpClient {
})) }))
} }
/// `GET v1/beacon/blinded_blocks/{block_id}`
///
/// Returns `Ok(None)` on a 404 error.
pub async fn get_beacon_blinded_blocks<T: EthSpec>(
&self,
block_id: BlockId,
) -> Result<Option<ExecutionOptimisticForkVersionedResponse<SignedBlindedBeaconBlock<T>>>, Error>
{
let path = self.get_beacon_blinded_blocks_path(block_id)?;
let response = match self.get_response(path, |b| b).await.optional()? {
Some(res) => res,
None => return Ok(None),
};
// 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, execution_optimistic) = match response.fork_name_from_header() {
Ok(Some(fork_name)) => {
let (data, (version, execution_optimistic)) =
map_fork_name_with!(fork_name, SignedBlindedBeaconBlock, {
let ExecutionOptimisticForkVersionedResponse {
version,
execution_optimistic,
data,
} = response.json().await?;
(data, (version, execution_optimistic))
});
(data, version, execution_optimistic)
}
Ok(None) | Err(_) => {
let ExecutionOptimisticForkVersionedResponse {
version,
execution_optimistic,
data,
} = response.json().await?;
(data, version, execution_optimistic)
}
};
Ok(Some(ExecutionOptimisticForkVersionedResponse {
version,
execution_optimistic,
data: block,
}))
}
/// `GET v1/beacon/blocks` (LEGACY) /// `GET v1/beacon/blocks` (LEGACY)
/// ///
/// Returns `Ok(None)` on a 404 error. /// Returns `Ok(None)` on a 404 error.
@ -714,6 +793,24 @@ impl BeaconNodeHttpClient {
.transpose() .transpose()
} }
/// `GET beacon/blinded_blocks/{block_id}` as SSZ
///
/// Returns `Ok(None)` on a 404 error.
pub async fn get_beacon_blinded_blocks_ssz<T: EthSpec>(
&self,
block_id: BlockId,
spec: &ChainSpec,
) -> Result<Option<SignedBlindedBeaconBlock<T>>, Error> {
let path = self.get_beacon_blinded_blocks_path(block_id)?;
self.get_bytes_opt_accept_header(path, Accept::Ssz, self.timeouts.get_beacon_blocks_ssz)
.await?
.map(|bytes| {
SignedBlindedBeaconBlock::from_ssz_bytes(&bytes, spec).map_err(Error::InvalidSsz)
})
.transpose()
}
/// `GET beacon/blocks/{block_id}/root` /// `GET beacon/blocks/{block_id}/root`
/// ///
/// Returns `Ok(None)` on a 404 error. /// Returns `Ok(None)` on a 404 error.

View File

@ -455,6 +455,11 @@ pub struct SyncCommitteesQuery {
pub epoch: Option<Epoch>, pub epoch: Option<Epoch>,
} }
#[derive(Serialize, Deserialize)]
pub struct RandaoQuery {
pub epoch: Option<Epoch>,
}
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct AttestationPoolQuery { pub struct AttestationPoolQuery {
pub slot: Option<Slot>, pub slot: Option<Slot>,
@ -486,6 +491,11 @@ pub struct SyncCommitteeByValidatorIndices {
pub validator_aggregates: Vec<SyncSubcommittee>, pub validator_aggregates: Vec<SyncSubcommittee>,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RandaoMix {
pub randao: Hash256,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(transparent)] #[serde(transparent)]
pub struct SyncSubcommittee { pub struct SyncSubcommittee {