diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 3aad79f3f..67f52a455 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -421,40 +421,69 @@ pub fn serve( }) }); - // GET beacon/states/{state_id}/validators + // GET beacon/states/{state_id}/validators?id,status let get_beacon_state_validators = beacon_states_path .clone() .and(warp::path("validators")) + .and(warp::query::()) .and(warp::path::end()) - .and_then(|state_id: StateId, chain: Arc>| { - blocking_json_task(move || { - state_id - .map_state(&chain, |state| { - let epoch = state.current_epoch(); - let finalized_epoch = state.finalized_checkpoint.epoch; - let far_future_epoch = chain.spec.far_future_epoch; + .and_then( + |state_id: StateId, chain: Arc>, query: api_types::ValidatorsQuery| { + blocking_json_task(move || { + state_id + .map_state(&chain, |state| { + let epoch = state.current_epoch(); + let finalized_epoch = state.finalized_checkpoint.epoch; + let far_future_epoch = chain.spec.far_future_epoch; - Ok(state - .validators - .iter() - .zip(state.balances.iter()) - .enumerate() - .map(|(index, (validator, balance))| api_types::ValidatorData { - index: index as u64, - balance: *balance, - status: api_types::ValidatorStatus::from_validator( - Some(validator), - epoch, - finalized_epoch, - far_future_epoch, - ), - validator: validator.clone(), - }) - .collect::>()) - }) - .map(api_types::GenericResponse::from) - }) - }); + Ok(state + .validators + .iter() + .zip(state.balances.iter()) + .enumerate() + // filter by validator id(s) if provided + .filter(|(index, (validator, _))| { + query.id.as_ref().map_or(true, |ids| { + ids.0.iter().any(|id| match id { + ValidatorId::PublicKey(pubkey) => { + &validator.pubkey == pubkey + } + ValidatorId::Index(param_index) => { + *param_index == *index as u64 + } + }) + }) + }) + // filter by status(es) if provided and map the result + .filter_map(|(index, (validator, balance))| { + let status = api_types::ValidatorStatus::from_validator( + Some(validator), + epoch, + finalized_epoch, + far_future_epoch, + ); + + if query + .status + .as_ref() + .map_or(true, |statuses| statuses.0.contains(&status)) + { + Some(api_types::ValidatorData { + index: index as u64, + balance: *balance, + status, + validator: validator.clone(), + }) + } else { + None + } + }) + .collect::>()) + }) + .map(api_types::GenericResponse::from) + }) + }, + ); // GET beacon/states/{state_id}/validators/{validator_id} let get_beacon_state_validators_id = beacon_states_path @@ -537,8 +566,8 @@ pub fn serve( } else { CommitteeCache::initialized(state, epoch, &chain.spec).map(Cow::Owned) } - .map_err(BeaconChainError::BeaconStateError) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(BeaconChainError::BeaconStateError) + .map_err(warp_utils::reject::beacon_chain_error)?; // Use either the supplied slot or all slots in the epoch. let slots = query.slot.map(|slot| vec![slot]).unwrap_or_else(|| { @@ -566,11 +595,11 @@ pub fn serve( let committee = committee_cache .get_beacon_committee(slot, index) .ok_or_else(|| { - warp_utils::reject::custom_bad_request(format!( - "committee index {} does not exist in epoch {}", - index, epoch - )) - })?; + warp_utils::reject::custom_bad_request(format!( + "committee index {} does not exist in epoch {}", + index, epoch + )) + })?; response.push(api_types::CommitteeData { index, @@ -1605,7 +1634,7 @@ pub fn serve( return Err(warp_utils::reject::object_invalid(format!( "gossip verification failed: {:?}", e - ))) + ))); } }; diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index 0b5154ce2..7ed4bae14 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -411,40 +411,87 @@ impl ApiTester { pub async fn test_beacon_states_validators(self) -> Self { for state_id in self.interesting_state_ids() { - let result = self - .client - .get_beacon_states_validators(state_id) - .await - .unwrap() - .map(|res| res.data); + for statuses in self.interesting_validator_statuses() { + for validator_indices in self.interesting_validator_indices() { + let state_opt = self.get_state(state_id); + let validators: Vec = match state_opt.as_ref() { + Some(state) => state.validators.clone().into(), + None => vec![], + }; + let validator_index_ids = validator_indices + .iter() + .cloned() + .map(|i| ValidatorId::Index(i)) + .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.clone()), + ) + }) + .collect::>(); - let expected = self.get_state(state_id).map(|state| { - let epoch = state.current_epoch(); - let finalized_epoch = state.finalized_checkpoint.epoch; - let far_future_epoch = self.chain.spec.far_future_epoch; + let result_index_ids = self + .client + .get_beacon_states_validators( + state_id, + Some(validator_index_ids.as_slice()), + None, + ) + .await + .unwrap() + .map(|res| res.data); - let mut validators = Vec::with_capacity(state.validators.len()); + let result_pubkey_ids = self + .client + .get_beacon_states_validators( + state_id, + Some(validator_pubkey_ids.as_slice()), + None, + ) + .await + .unwrap() + .map(|res| res.data); - for i in 0..state.validators.len() { - let validator = state.validators[i].clone(); + let expected = state_opt.map(|state| { + let epoch = state.current_epoch(); + let finalized_epoch = state.finalized_checkpoint.epoch; + let far_future_epoch = self.chain.spec.far_future_epoch; - validators.push(ValidatorData { - index: i as u64, - balance: state.balances[i], - status: ValidatorStatus::from_validator( - Some(&validator), - epoch, - finalized_epoch, - far_future_epoch, - ), - validator, - }) + let mut validators = Vec::with_capacity(validator_indices.len()); + + for i in validator_indices { + if i >= state.validators.len() as u64 { + continue; + } + let validator = state.validators[i as usize].clone(); + let status = ValidatorStatus::from_validator( + Some(&validator), + epoch, + finalized_epoch, + far_future_epoch, + ); + if statuses.contains(&status) || statuses.is_empty() { + validators.push(ValidatorData { + index: i as u64, + balance: state.balances[i as usize], + status, + validator, + }); + } + } + + validators + }); + + assert_eq!(result_index_ids, expected, "{:?}", state_id); + assert_eq!(result_pubkey_ids, expected, "{:?}", state_id); } - - validators - }); - - assert_eq!(result, expected, "{:?}", state_id); + } } self @@ -1149,6 +1196,28 @@ impl ApiTester { interesting } + fn interesting_validator_statuses(&self) -> Vec> { + let interesting = vec![ + vec![], + vec![ValidatorStatus::Active], + vec![ + ValidatorStatus::Unknown, + ValidatorStatus::WaitingForEligibility, + ValidatorStatus::WaitingForFinality, + ValidatorStatus::WaitingInQueue, + ValidatorStatus::StandbyForActive, + ValidatorStatus::Active, + ValidatorStatus::ActiveAwaitingVoluntaryExit, + ValidatorStatus::ActiveAwaitingSlashedExit, + ValidatorStatus::ExitedVoluntarily, + ValidatorStatus::ExitedSlashed, + ValidatorStatus::Withdrawable, + ValidatorStatus::Withdrawn, + ], + ]; + interesting + } + pub async fn test_get_validator_duties_attester(self) -> Self { let current_epoch = self.chain.epoch().unwrap().as_u64(); diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 8a0ffbe35..b9fe29e2e 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -210,12 +210,14 @@ impl BeaconNodeHttpClient { self.get_opt(path).await } - /// `GET beacon/states/{state_id}/validators` + /// `GET beacon/states/{state_id}/validators?id,status` /// /// Returns `Ok(None)` on a 404 error. pub async fn get_beacon_states_validators( &self, state_id: StateId, + ids: Option<&[ValidatorId]>, + statuses: Option<&[ValidatorStatus]>, ) -> Result>>, Error> { let mut path = self.eth_path()?; @@ -226,6 +228,24 @@ impl BeaconNodeHttpClient { .push(&state_id.to_string()) .push("validators"); + if let Some(ids) = ids { + let id_string = ids + .iter() + .map(|i| i.to_string()) + .collect::>() + .join(","); + path.query_pairs_mut().append_pair("id", &id_string); + } + + if let Some(statuses) = statuses { + let status_string = statuses + .iter() + .map(|i| i.to_string()) + .collect::>() + .join(","); + path.query_pairs_mut().append_pair("status", &status_string); + } + self.get_opt(path).await } diff --git a/common/eth2/src/types.rs b/common/eth2/src/types.rs index 02fdc5ca4..a300b875e 100644 --- a/common/eth2/src/types.rs +++ b/common/eth2/src/types.rs @@ -165,7 +165,7 @@ pub struct FinalityCheckpointsData { pub finalized: Checkpoint, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ValidatorId { PublicKey(PublicKeyBytes), Index(u64), @@ -211,17 +211,18 @@ pub struct ValidatorData { // // https://hackmd.io/bQxMDRt1RbS1TLno8K4NPg?view #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] pub enum ValidatorStatus { Unknown, WaitingForEligibility, WaitingForFinality, WaitingInQueue, - StandbyForActive(Epoch), + StandbyForActive, Active, - ActiveAwaitingVoluntaryExit(Epoch), - ActiveAwaitingSlashedExit(Epoch), - ExitedVoluntarily(Epoch), - ExitedSlashed(Epoch), + ActiveAwaitingVoluntaryExit, + ActiveAwaitingSlashedExit, + ExitedVoluntarily, + ExitedSlashed, Withdrawable, Withdrawn, } @@ -238,22 +239,22 @@ impl ValidatorStatus { ValidatorStatus::Withdrawable } else if validator.is_exited_at(epoch) { if validator.slashed { - ValidatorStatus::ExitedSlashed(validator.withdrawable_epoch) + ValidatorStatus::ExitedSlashed } else { - ValidatorStatus::ExitedVoluntarily(validator.withdrawable_epoch) + ValidatorStatus::ExitedVoluntarily } } else if validator.is_active_at(epoch) { if validator.exit_epoch < far_future_epoch { if validator.slashed { - ValidatorStatus::ActiveAwaitingSlashedExit(validator.exit_epoch) + ValidatorStatus::ActiveAwaitingSlashedExit } else { - ValidatorStatus::ActiveAwaitingVoluntaryExit(validator.exit_epoch) + ValidatorStatus::ActiveAwaitingVoluntaryExit } } else { ValidatorStatus::Active } } else if validator.activation_epoch < far_future_epoch { - ValidatorStatus::StandbyForActive(validator.activation_epoch) + ValidatorStatus::StandbyForActive } else if validator.activation_eligibility_epoch < far_future_epoch { if finalized_epoch < validator.activation_eligibility_epoch { ValidatorStatus::WaitingForFinality @@ -269,12 +270,61 @@ impl ValidatorStatus { } } +impl FromStr for ValidatorStatus { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "unknown" => Ok(ValidatorStatus::Unknown), + "waiting_for_eligibility" => Ok(ValidatorStatus::WaitingForEligibility), + "waiting_for_finality" => Ok(ValidatorStatus::WaitingForFinality), + "waiting_in_queue" => Ok(ValidatorStatus::WaitingInQueue), + "standby_for_active" => Ok(ValidatorStatus::StandbyForActive), + "active" => Ok(ValidatorStatus::Active), + "active_awaiting_voluntary_exit" => Ok(ValidatorStatus::ActiveAwaitingVoluntaryExit), + "active_awaiting_slashed_exit" => Ok(ValidatorStatus::ActiveAwaitingSlashedExit), + "exited_voluntarily" => Ok(ValidatorStatus::ExitedVoluntarily), + "exited_slashed" => Ok(ValidatorStatus::ExitedSlashed), + "withdrawable" => Ok(ValidatorStatus::Withdrawable), + "withdrawn" => Ok(ValidatorStatus::Withdrawn), + _ => Err(format!("{} cannot be parsed as a validator status.", s)), + } + } +} + +impl fmt::Display for ValidatorStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ValidatorStatus::Unknown => write!(f, "unknown"), + ValidatorStatus::WaitingForEligibility => write!(f, "waiting_for_eligibility"), + ValidatorStatus::WaitingForFinality => write!(f, "waiting_for_finality"), + ValidatorStatus::WaitingInQueue => write!(f, "waiting_in_queue"), + ValidatorStatus::StandbyForActive => write!(f, "standby_for_active"), + ValidatorStatus::Active => write!(f, "active"), + ValidatorStatus::ActiveAwaitingVoluntaryExit => { + write!(f, "active_awaiting_voluntary_exit") + } + ValidatorStatus::ActiveAwaitingSlashedExit => write!(f, "active_awaiting_slashed_exit"), + ValidatorStatus::ExitedVoluntarily => write!(f, "exited_voluntarily"), + ValidatorStatus::ExitedSlashed => write!(f, "exited_slashed"), + ValidatorStatus::Withdrawable => write!(f, "withdrawable"), + ValidatorStatus::Withdrawn => write!(f, "withdrawn"), + } + } +} + #[derive(Serialize, Deserialize)] pub struct CommitteesQuery { pub slot: Option, pub index: Option, } +#[derive(Deserialize)] +pub struct ValidatorsQuery { + pub id: Option>, + pub status: Option>, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct CommitteeData { #[serde(with = "serde_utils::quoted_u64")]