diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index a3e214de86..a627fb0353 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -48,7 +48,8 @@ use directory::DEFAULT_ROOT_DIR; use eth2::types::{ self as api_types, BroadcastValidation, ContextDeserialize, EndpointVersion, ForkChoice, ForkChoiceNode, LightClientUpdatesQuery, PublishBlockRequest, StateId as CoreStateId, - ValidatorBalancesRequestBody, ValidatorId, ValidatorStatus, ValidatorsRequestBody, + ValidatorBalancesRequestBody, ValidatorId, ValidatorIdentitiesRequestBody, ValidatorStatus, + ValidatorsRequestBody, }; use eth2::{CONSENSUS_VERSION_HEADER, CONTENT_TYPE_HEADER, SSZ_CONTENT_TYPE_HEADER}; use health_metrics::observe::Observe; @@ -702,6 +703,34 @@ pub fn serve( }, ); + // POST beacon/states/{state_id}/validator_identities + let post_beacon_state_validator_identities = beacon_states_path + .clone() + .and(warp::path("validator_identities")) + .and(warp::path::end()) + .and(warp_utils::json::json_no_body()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>, + query: ValidatorIdentitiesRequestBody| { + // Prioritise requests for validators at the head. These should be fast to service + // and could be required by the validator client. + let priority = if let StateId(eth2::types::StateId::Head) = state_id { + Priority::P0 + } else { + Priority::P1 + }; + task_spawner.blocking_json_task(priority, move || { + crate::validators::get_beacon_state_validator_identities( + state_id, + chain, + Some(&query.ids), + ) + }) + }, + ); + // GET beacon/states/{state_id}/validators?id,status let get_beacon_state_validators = beacon_states_path .clone() @@ -4852,6 +4881,7 @@ pub fn serve( .uor(post_beacon_pool_bls_to_execution_changes) .uor(post_beacon_state_validators) .uor(post_beacon_state_validator_balances) + .uor(post_beacon_state_validator_identities) .uor(post_beacon_rewards_attestations) .uor(post_beacon_rewards_sync_committee) .uor(post_validator_duties_attester) diff --git a/beacon_node/http_api/src/validators.rs b/beacon_node/http_api/src/validators.rs index 90ddd1ee8f..755b540502 100644 --- a/beacon_node/http_api/src/validators.rs +++ b/beacon_node/http_api/src/validators.rs @@ -2,7 +2,7 @@ use crate::state_id::StateId; use beacon_chain::{BeaconChain, BeaconChainTypes}; use eth2::types::{ self as api_types, ExecutionOptimisticFinalizedResponse, ValidatorBalanceData, ValidatorData, - ValidatorId, ValidatorStatus, + ValidatorId, ValidatorIdentityData, ValidatorStatus, }; use std::{collections::HashSet, sync::Arc}; @@ -119,3 +119,51 @@ pub fn get_beacon_state_validator_balances( finalized: Some(finalized), }) } + +pub fn get_beacon_state_validator_identities( + state_id: StateId, + chain: Arc>, + optional_ids: Option<&[ValidatorId]>, +) -> Result>, warp::Rejection> { + let (data, execution_optimistic, finalized) = state_id + .map_state_and_execution_optimistic_and_finalized( + &chain, + |state, execution_optimistic, finalized| { + let ids_filter_set: Option> = match optional_ids { + // Same logic as validator_balances endpoint above + Some([]) => None, + Some(ids) => Some(HashSet::from_iter(ids.iter())), + None => None, + }; + + Ok(( + // From the BeaconState, extract the Validator data and convert it into ValidatorIdentityData type + state + .validators() + .iter() + .enumerate() + // filter by validator id(s) if provided + .filter(|(index, validator)| { + ids_filter_set.as_ref().is_none_or(|ids_set| { + ids_set.contains(&ValidatorId::PublicKey(validator.pubkey)) + || ids_set.contains(&ValidatorId::Index(*index as u64)) + }) + }) + .map(|(index, validator)| ValidatorIdentityData { + index: index as u64, + pubkey: validator.pubkey, + activation_epoch: validator.activation_epoch, + }) + .collect::>(), + execution_optimistic, + finalized, + )) + }, + )?; + + Ok(api_types::ExecutionOptimisticFinalizedResponse { + data, + execution_optimistic: Some(execution_optimistic), + finalized: Some(finalized), + }) +} diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index fe3e374727..955b44c36c 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -964,6 +964,87 @@ impl ApiTester { self } + pub async fn test_beacon_states_validator_identities(self) -> Self { + for state_id in self.interesting_state_ids() { + for validator_indices in self.interesting_validator_indices() { + let state_opt = state_id.state(&self.chain).ok(); + let validators: Vec = match state_opt.as_ref() { + Some((state, _execution_optimistic, _finalized)) => { + state.validators().clone().to_vec() + } + None => vec![], + }; + + let validator_index_ids = validator_indices + .iter() + .cloned() + .map(ValidatorId::Index) + .collect::>(); + + let validator_pubkey_ids = validator_indices + .iter() + .cloned() + .map(|i| { + ValidatorId::PublicKey( + validators + .get(i as usize) + .map_or(PublicKeyBytes::empty(), |val| val.pubkey), + ) + }) + .collect::>(); + + let result_index_ids = self + .client + .post_beacon_states_validator_identities(state_id.0, validator_index_ids) + .await + .unwrap() + .map(|res| res.data); + let result_pubkey_ids = self + .client + .post_beacon_states_validator_identities(state_id.0, validator_pubkey_ids) + .await + .unwrap() + .map(|res| res.data); + + let expected = state_opt.map(|(state, _execution_optimistic, _finalized)| { + // If validator_indices is empty, return identities for all validators + if validator_indices.is_empty() { + state + .validators() + .iter() + .enumerate() + .map(|(index, validator)| ValidatorIdentityData { + index: index as u64, + pubkey: validator.pubkey, + activation_epoch: validator.activation_epoch, + }) + .collect() + } else { + let mut validators = Vec::with_capacity(validator_indices.len()); + + for i in validator_indices { + if i < state.validators().len() as u64 { + // access each validator, and then transform the data into ValidatorIdentityData + let validator = state.validators().get(i as usize).unwrap(); + validators.push(ValidatorIdentityData { + index: i, + pubkey: validator.pubkey, + activation_epoch: validator.activation_epoch, + }); + } + } + + validators + } + }); + + assert_eq!(result_index_ids, expected, "{:?}", state_id); + assert_eq!(result_pubkey_ids, expected, "{:?}", state_id); + } + } + self + } + pub async fn test_beacon_states_validators(self) -> Self { for state_id in self.interesting_state_ids() { for statuses in self.interesting_validator_statuses() { @@ -6685,6 +6766,8 @@ async fn beacon_get_state_info() { .await .test_beacon_states_validator_balances() .await + .test_beacon_states_validator_identities() + .await .test_beacon_states_committees() .await .test_beacon_states_validator_id() diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 70fa52e60a..a129f9c4fa 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -701,6 +701,29 @@ impl BeaconNodeHttpClient { self.post_with_opt_response(path, &request).await } + /// `POST beacon/states/{state_id}/validator_identities` + /// + /// Returns `Ok(None)` on a 404 error. + pub async fn post_beacon_states_validator_identities( + &self, + state_id: StateId, + ids: Vec, + ) -> Result>>, 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("validator_identities"); + + let request = ValidatorIdentitiesRequestBody { ids }; + + self.post_with_opt_response(path, &request).await + } + /// `GET beacon/states/{state_id}/validators?id,status` /// /// Returns `Ok(None)` on a 404 error. diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index fa9f17f5cb..07b5cb5016 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -349,6 +349,14 @@ pub struct ValidatorBalanceData { pub balance: u64, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ValidatorIdentityData { + #[serde(with = "serde_utils::quoted_u64")] + pub index: u64, + pub pubkey: PublicKeyBytes, + pub activation_epoch: Epoch, +} + // Implemented according to what is described here: // // https://hackmd.io/ofFJ5gOmQpu1jjHilHbdQQ @@ -694,6 +702,12 @@ pub struct ValidatorBalancesRequestBody { pub ids: Vec, } +#[derive(Clone, Default, Serialize, Deserialize)] +#[serde(transparent)] +pub struct ValidatorIdentitiesRequestBody { + pub ids: Vec, +} + #[derive(Clone, Deserialize)] #[serde(deny_unknown_fields)] pub struct BlobIndicesQuery {