Skip to content

Commit

Permalink
Blinded block and RANDAO APIs (#3571)
Browse files Browse the repository at this point in the history
## Issue Addressed

ethereum/beacon-APIs#241
ethereum/beacon-APIs#242

## Proposed Changes

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


Co-authored-by: realbigsean <sean@sigmaprime.io>
  • Loading branch information
michaelsproul and realbigsean committed Nov 10, 2022
1 parent 266d765 commit 6558864
Show file tree
Hide file tree
Showing 4 changed files with 296 additions and 0 deletions.
79 changes: 79 additions & 0 deletions beacon_node/http_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
//
// Note: this endpoint only returns information about blocks in the canonical chain. Given that
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -3164,10 +3240,12 @@ pub fn serve<T: BeaconChainTypes>(
.or(get_beacon_state_validators.boxed())
.or(get_beacon_state_committees.boxed())
.or(get_beacon_state_sync_committees.boxed())
.or(get_beacon_state_randao.boxed())
.or(get_beacon_headers.boxed())
.or(get_beacon_headers_block_id.boxed())
.or(get_beacon_block.boxed())
.or(get_beacon_block_attestations.boxed())
.or(get_beacon_blinded_block.boxed())
.or(get_beacon_block_root.boxed())
.or(get_beacon_pool_attestations.boxed())
.or(get_beacon_pool_attester_slashings.boxed())
Expand Down Expand Up @@ -3212,6 +3290,7 @@ pub fn serve<T: BeaconChainTypes>(
.or(get_lighthouse_merge_readiness.boxed())
.or(get_events.boxed()),
)
.boxed()
.or(warp::post().and(
post_beacon_blocks
.boxed()
Expand Down
110 changes: 110 additions & 0 deletions beacon_node/http_api/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,36 @@ impl ApiTester {
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 {
for slot in 0..CHAIN_LENGTH {
let slot = Slot::from(slot);
Expand Down Expand Up @@ -1016,6 +1046,82 @@ impl ApiTester {
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 {
for block_id in self.interesting_block_ids() {
let result = self
Expand Down Expand Up @@ -3696,6 +3802,8 @@ async fn beacon_get() {
.await
.test_beacon_states_validator_id()
.await
.test_beacon_states_randao()
.await
.test_beacon_headers_all_slots()
.await
.test_beacon_headers_all_parents()
Expand All @@ -3704,6 +3812,8 @@ async fn beacon_get() {
.await
.test_beacon_blocks()
.await
.test_beacon_blinded_blocks()
.await
.test_beacon_blocks_attestations()
.await
.test_beacon_blocks_root()
Expand Down
97 changes: 97 additions & 0 deletions common/eth2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,29 @@ impl BeaconNodeHttpClient {
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}`
///
/// Returns `Ok(None)` on a 404 error.
Expand Down Expand Up @@ -636,6 +659,17 @@ impl BeaconNodeHttpClient {
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`
///
/// Returns `Ok(None)` on a 404 error.
Expand Down Expand Up @@ -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)
///
/// Returns `Ok(None)` on a 404 error.
Expand Down Expand Up @@ -714,6 +793,24 @@ impl BeaconNodeHttpClient {
.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`
///
/// Returns `Ok(None)` on a 404 error.
Expand Down
10 changes: 10 additions & 0 deletions common/eth2/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,11 @@ pub struct SyncCommitteesQuery {
pub epoch: Option<Epoch>,
}

#[derive(Serialize, Deserialize)]
pub struct RandaoQuery {
pub epoch: Option<Epoch>,
}

#[derive(Serialize, Deserialize)]
pub struct AttestationPoolQuery {
pub slot: Option<Slot>,
Expand Down Expand Up @@ -486,6 +491,11 @@ pub struct SyncCommitteeByValidatorIndices {
pub validator_aggregates: Vec<SyncSubcommittee>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RandaoMix {
pub randao: Hash256,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SyncSubcommittee {
Expand Down

0 comments on commit 6558864

Please sign in to comment.