diff --git a/beacon_node/beacon_chain/src/beacon_chain.rs b/beacon_node/beacon_chain/src/beacon_chain.rs index f54772ec0..0b3fffda3 100644 --- a/beacon_node/beacon_chain/src/beacon_chain.rs +++ b/beacon_node/beacon_chain/src/beacon_chain.rs @@ -376,6 +376,19 @@ impl BeaconChain { Ok(self.store.get(block_root)?) } + /// Returns the state at the given root, if any. + /// + /// ## Errors + /// + /// May return a database error. + pub fn get_state( + &self, + state_root: &Hash256, + slot: Option, + ) -> Result>, Error> { + Ok(self.store.get_state(state_root, slot)?) + } + /// Returns the block at the given root, if any. /// /// ## Errors diff --git a/beacon_node/rest_api/src/beacon.rs b/beacon_node/rest_api/src/beacon.rs index a73ade89b..8b5438100 100644 --- a/beacon_node/rest_api/src/beacon.rs +++ b/beacon_node/rest_api/src/beacon.rs @@ -1,16 +1,22 @@ use crate::helpers::*; use crate::response_builder::ResponseBuilder; -use crate::{ApiError, ApiResult, UrlQuery}; +use crate::validator::get_state_for_epoch; +use crate::{ApiError, ApiResult, BoxFut, UrlQuery}; use beacon_chain::{BeaconChain, BeaconChainTypes}; +use futures::{Future, Stream}; use hyper::{Body, Request}; use serde::{Deserialize, Serialize}; -use ssz_derive::Encode; +use ssz_derive::{Decode, Encode}; use std::sync::Arc; use store::Store; -use types::{BeaconBlock, BeaconState, Epoch, EthSpec, Hash256, Slot, Validator}; +use types::{ + BeaconBlock, BeaconState, CommitteeIndex, EthSpec, Hash256, PublicKeyBytes, RelativeEpoch, + Slot, Validator, +}; -#[derive(Serialize, Deserialize, Encode)] -pub struct HeadResponse { +/// Information about the block and state that are at head of the beacon chain. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)] +pub struct CanonicalHeadResponse { pub slot: Slot, pub block_root: Hash256, pub state_root: Hash256, @@ -29,7 +35,7 @@ pub fn get_head( ) -> ApiResult { let chain_head = beacon_chain.head(); - let head = HeadResponse { + let head = CanonicalHeadResponse { slot: chain_head.beacon_state.slot, block_root: chain_head.beacon_block_root, state_root: chain_head.beacon_state_root, @@ -56,10 +62,12 @@ pub fn get_head( ResponseBuilder::new(&req)?.body(&head) } -#[derive(Serialize, Deserialize, Encode)] +/// Information about a block that is at the head of a chain. May or may not represent the +/// canonical head. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)] pub struct HeadBeaconBlock { - beacon_block_root: Hash256, - beacon_block_slot: Slot, + pub beacon_block_root: Hash256, + pub beacon_block_slot: Slot, } /// HTTP handler to return a list of head BeaconBlocks. @@ -79,7 +87,7 @@ pub fn get_heads( ResponseBuilder::new(&req)?.body(&heads) } -#[derive(Serialize, Encode)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)] #[serde(bound = "T: EthSpec")] pub struct BlockResponse { pub root: Hash256, @@ -147,40 +155,254 @@ pub fn get_fork( ResponseBuilder::new(&req)?.body(&beacon_chain.head().beacon_state.fork) } -/// HTTP handler to return the set of validators for an `Epoch` +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)] +pub struct ValidatorResponse { + pub pubkey: PublicKeyBytes, + pub validator_index: Option, + pub balance: Option, + pub validator: Option, +} + +/// HTTP handler to which accepts a query string of a list of validator pubkeys and maps it to a +/// `ValidatorResponse`. /// -/// The `Epoch` parameter can be any epoch number. If it is not specified, -/// the current epoch is assumed. +/// This method is limited to as many `pubkeys` that can fit in a URL. See `post_validators` for +/// doing bulk requests. pub fn get_validators( req: Request, beacon_chain: Arc>, ) -> ApiResult { - let epoch = match UrlQuery::from_request(&req) { - // We have some parameters, so make sure it's the epoch one and parse it - Ok(query) => query - .only_one("epoch")? - .parse::() - .map(Epoch::from) - .map_err(|e| { - ApiError::BadRequest(format!("Invalid epoch parameter, must be a u64. {:?}", e)) - })?, - // In this case, our url query did not contain any parameters, so we take the default - Err(_) => beacon_chain.epoch().map_err(|e| { - ApiError::ServerError(format!("Unable to determine current epoch: {:?}", e)) - })?, + let query = UrlQuery::from_request(&req)?; + + let validator_pubkeys = query + .all_of("validator_pubkeys")? + .iter() + .map(|validator_pubkey_str| parse_pubkey_bytes(validator_pubkey_str)) + .collect::, _>>()?; + + let state_root_opt = if let Some((_key, value)) = query.first_of_opt(&["state_root"]) { + Some(parse_root(&value)?) + } else { + None }; - let all_validators = &beacon_chain.head().beacon_state.validators; - let active_vals: Vec = all_validators - .iter() - .filter(|v| v.is_active_at(epoch)) - .cloned() - .collect(); + let validators = + validator_responses_by_pubkey(beacon_chain, state_root_opt, validator_pubkeys)?; - ResponseBuilder::new(&req)?.body(&active_vals) + ResponseBuilder::new(&req)?.body(&validators) } -#[derive(Serialize, Encode)] +/// HTTP handler to return all validators, each as a `ValidatorResponse`. +pub fn get_all_validators( + req: Request, + beacon_chain: Arc>, +) -> ApiResult { + let query = UrlQuery::from_request(&req)?; + + let state_root_opt = if let Some((_key, value)) = query.first_of_opt(&["state_root"]) { + Some(parse_root(&value)?) + } else { + None + }; + + let mut state = get_state_from_root_opt(&beacon_chain, state_root_opt)?; + state.update_pubkey_cache()?; + + let validators = state + .validators + .iter() + .map(|validator| validator_response_by_pubkey(&state, validator.pubkey.clone())) + .collect::, _>>()?; + + ResponseBuilder::new(&req)?.body(&validators) +} + +/// HTTP handler to return all active validators, each as a `ValidatorResponse`. +pub fn get_active_validators( + req: Request, + beacon_chain: Arc>, +) -> ApiResult { + let query = UrlQuery::from_request(&req)?; + + let state_root_opt = if let Some((_key, value)) = query.first_of_opt(&["state_root"]) { + Some(parse_root(&value)?) + } else { + None + }; + + let mut state = get_state_from_root_opt(&beacon_chain, state_root_opt)?; + state.update_pubkey_cache()?; + + let validators = state + .validators + .iter() + .filter(|validator| validator.is_active_at(state.current_epoch())) + .map(|validator| validator_response_by_pubkey(&state, validator.pubkey.clone())) + .collect::, _>>()?; + + ResponseBuilder::new(&req)?.body(&validators) +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)] +pub struct ValidatorRequest { + /// If set to `None`, uses the canonical head state. + pub state_root: Option, + pub pubkeys: Vec, +} + +/// HTTP handler to which accepts a `ValidatorRequest` and returns a `ValidatorResponse` for +/// each of the given `pubkeys`. When `state_root` is `None`, the canonical head is used. +/// +/// This method allows for a basically unbounded list of `pubkeys`, where as the `get_validators` +/// request is limited by the max number of pubkeys you can fit in a URL. +pub fn post_validators( + req: Request, + beacon_chain: Arc>, +) -> BoxFut { + let response_builder = ResponseBuilder::new(&req); + + let future = req + .into_body() + .concat2() + .map_err(|e| ApiError::ServerError(format!("Unable to get request body: {:?}", e))) + .and_then(|chunks| { + serde_json::from_slice::(&chunks).map_err(|e| { + ApiError::BadRequest(format!( + "Unable to parse JSON into ValidatorRequest: {:?}", + e + )) + }) + }) + .and_then(|bulk_request| { + validator_responses_by_pubkey( + beacon_chain, + bulk_request.state_root, + bulk_request.pubkeys, + ) + }) + .and_then(|validators| response_builder?.body(&validators)); + + Box::new(future) +} + +/// Returns either the state given by `state_root_opt`, or the canonical head state if it is +/// `None`. +fn get_state_from_root_opt( + beacon_chain: &BeaconChain, + state_root_opt: Option, +) -> Result, ApiError> { + if let Some(state_root) = state_root_opt { + beacon_chain + .get_state(&state_root, None) + .map_err(|e| { + ApiError::ServerError(format!( + "Database error when reading state root {}: {:?}", + state_root, e + )) + })? + .ok_or_else(|| ApiError::NotFound(format!("No state exists with root: {}", state_root))) + } else { + Ok(beacon_chain.head().beacon_state) + } +} + +/// Maps a vec of `validator_pubkey` to a vec of `ValidatorResponse`, using the state at the given +/// `state_root`. If `state_root.is_none()`, uses the canonial head state. +fn validator_responses_by_pubkey( + beacon_chain: Arc>, + state_root_opt: Option, + validator_pubkeys: Vec, +) -> Result, ApiError> { + let mut state = get_state_from_root_opt(&beacon_chain, state_root_opt)?; + state.update_pubkey_cache()?; + + validator_pubkeys + .into_iter() + .map(|validator_pubkey| validator_response_by_pubkey(&state, validator_pubkey)) + .collect::, ApiError>>() +} + +/// Maps a `validator_pubkey` to a `ValidatorResponse`, using the given state. +/// +/// The provided `state` must have a fully up-to-date pubkey cache. +fn validator_response_by_pubkey( + state: &BeaconState, + validator_pubkey: PublicKeyBytes, +) -> Result { + let validator_index_opt = state + .get_validator_index(&validator_pubkey) + .map_err(|e| ApiError::ServerError(format!("Unable to read pubkey cache: {:?}", e)))?; + + if let Some(validator_index) = validator_index_opt { + let balance = state.balances.get(validator_index).ok_or_else(|| { + ApiError::ServerError(format!("Invalid balances index: {:?}", validator_index)) + })?; + + let validator = state + .validators + .get(validator_index) + .ok_or_else(|| { + ApiError::ServerError(format!("Invalid validator index: {:?}", validator_index)) + })? + .clone(); + + Ok(ValidatorResponse { + pubkey: validator_pubkey, + validator_index: Some(validator_index), + balance: Some(*balance), + validator: Some(validator), + }) + } else { + Ok(ValidatorResponse { + pubkey: validator_pubkey, + validator_index: None, + balance: None, + validator: None, + }) + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)] +pub struct Committee { + pub slot: Slot, + pub index: CommitteeIndex, + pub committee: Vec, +} + +/// HTTP handler +pub fn get_committees( + req: Request, + beacon_chain: Arc>, +) -> ApiResult { + let query = UrlQuery::from_request(&req)?; + + let epoch = query.epoch()?; + + let mut state = get_state_for_epoch(&beacon_chain, epoch)?; + + let relative_epoch = RelativeEpoch::from_epoch(state.current_epoch(), epoch).map_err(|e| { + ApiError::ServerError(format!("Failed to get state suitable for epoch: {:?}", e)) + })?; + + state + .build_committee_cache(relative_epoch, &beacon_chain.spec) + .map_err(|e| ApiError::ServerError(format!("Unable to build committee cache: {:?}", e)))?; + + let committees = state + .get_beacon_committees_at_epoch(relative_epoch) + .map_err(|e| ApiError::ServerError(format!("Unable to get all committees: {:?}", e)))? + .into_iter() + .map(|c| Committee { + slot: c.slot, + index: c.index, + committee: c.committee.to_vec(), + }) + .collect::>(); + + ResponseBuilder::new(&req)?.body(&committees) +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Encode, Decode)] #[serde(bound = "T: EthSpec")] pub struct StateResponse { pub root: Hash256, @@ -251,19 +473,10 @@ pub fn get_state_root( ResponseBuilder::new(&req)?.body(&root) } -/// HTTP handler to return the highest finalized slot. -pub fn get_current_finalized_checkpoint( - req: Request, - beacon_chain: Arc>, -) -> ApiResult { - let head_state = beacon_chain.head().beacon_state; - - let checkpoint = head_state.finalized_checkpoint.clone(); - - ResponseBuilder::new(&req)?.body(&checkpoint) -} - /// HTTP handler to return a `BeaconState` at the genesis block. +/// +/// This is an undocumented convenience method used during testing. For production, simply do a +/// state request at slot 0. pub fn get_genesis_state( req: Request, beacon_chain: Arc>, diff --git a/beacon_node/rest_api/src/consensus.rs b/beacon_node/rest_api/src/consensus.rs new file mode 100644 index 000000000..5d3749fc0 --- /dev/null +++ b/beacon_node/rest_api/src/consensus.rs @@ -0,0 +1,199 @@ +use crate::helpers::*; +use crate::response_builder::ResponseBuilder; +use crate::{ApiError, ApiResult, BoxFut, UrlQuery}; +use beacon_chain::{BeaconChain, BeaconChainTypes}; +use futures::{Future, Stream}; +use hyper::{Body, Request}; +use serde::{Deserialize, Serialize}; +use ssz_derive::{Decode, Encode}; +use state_processing::per_epoch_processing::{TotalBalances, ValidatorStatus, ValidatorStatuses}; +use std::sync::Arc; +use types::{Epoch, EthSpec, PublicKeyBytes}; + +/// The results of validators voting during an epoch. +/// +/// Provides information about the current and previous epochs. +#[derive(Serialize, Deserialize, Encode, Decode)] +pub struct VoteCount { + /// The total effective balance of all active validators during the _current_ epoch. + pub current_epoch_active_gwei: u64, + /// The total effective balance of all active validators during the _previous_ epoch. + pub previous_epoch_active_gwei: u64, + /// The total effective balance of all validators who attested during the _current_ epoch. + pub current_epoch_attesting_gwei: u64, + /// The total effective balance of all validators who attested during the _current_ epoch and + /// agreed with the state about the beacon block at the first slot of the _current_ epoch. + pub current_epoch_target_attesting_gwei: u64, + /// The total effective balance of all validators who attested during the _previous_ epoch. + pub previous_epoch_attesting_gwei: u64, + /// The total effective balance of all validators who attested during the _previous_ epoch and + /// agreed with the state about the beacon block at the first slot of the _previous_ epoch. + pub previous_epoch_target_attesting_gwei: u64, + /// The total effective balance of all validators who attested during the _previous_ epoch and + /// agreed with the state about the beacon block at the time of attestation. + pub previous_epoch_head_attesting_gwei: u64, +} + +impl Into for TotalBalances { + fn into(self) -> VoteCount { + VoteCount { + current_epoch_active_gwei: self.current_epoch, + previous_epoch_active_gwei: self.previous_epoch, + current_epoch_attesting_gwei: self.current_epoch_attesters, + current_epoch_target_attesting_gwei: self.current_epoch_target_attesters, + previous_epoch_attesting_gwei: self.previous_epoch_attesters, + previous_epoch_target_attesting_gwei: self.previous_epoch_target_attesters, + previous_epoch_head_attesting_gwei: self.previous_epoch_head_attesters, + } + } +} + +/// HTTP handler return a `VoteCount` for some given `Epoch`. +pub fn get_vote_count( + req: Request, + beacon_chain: Arc>, +) -> ApiResult { + let query = UrlQuery::from_request(&req)?; + + let epoch = query.epoch()?; + // This is the last slot of the given epoch (one prior to the first slot of the next epoch). + let target_slot = (epoch + 1).start_slot(T::EthSpec::slots_per_epoch()) - 1; + + let (_root, state) = state_at_slot(&beacon_chain, target_slot)?; + let spec = &beacon_chain.spec; + + let mut validator_statuses = ValidatorStatuses::new(&state, spec)?; + validator_statuses.process_attestations(&state, spec)?; + + let report: VoteCount = validator_statuses.total_balances.into(); + + ResponseBuilder::new(&req)?.body(&report) +} + +#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)] +pub struct IndividualVotesRequest { + pub epoch: Epoch, + pub pubkeys: Vec, +} + +#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)] +pub struct IndividualVote { + /// True if the validator has been slashed, ever. + pub is_slashed: bool, + /// True if the validator can withdraw in the current epoch. + pub is_withdrawable_in_current_epoch: bool, + /// True if the validator was active in the state's _current_ epoch. + pub is_active_in_current_epoch: bool, + /// True if the validator was active in the state's _previous_ epoch. + pub is_active_in_previous_epoch: bool, + /// The validator's effective balance in the _current_ epoch. + pub current_epoch_effective_balance_gwei: u64, + /// True if the validator had an attestation included in the _current_ epoch. + pub is_current_epoch_attester: bool, + /// True if the validator's beacon block root attestation for the first slot of the _current_ + /// epoch matches the block root known to the state. + pub is_current_epoch_target_attester: bool, + /// True if the validator had an attestation included in the _previous_ epoch. + pub is_previous_epoch_attester: bool, + /// True if the validator's beacon block root attestation for the first slot of the _previous_ + /// epoch matches the block root known to the state. + pub is_previous_epoch_target_attester: bool, + /// True if the validator's beacon block root attestation in the _previous_ epoch at the + /// attestation's slot (`attestation_data.slot`) matches the block root known to the state. + pub is_previous_epoch_head_attester: bool, +} + +impl Into for ValidatorStatus { + fn into(self) -> IndividualVote { + IndividualVote { + is_slashed: self.is_slashed, + is_withdrawable_in_current_epoch: self.is_withdrawable_in_current_epoch, + is_active_in_current_epoch: self.is_active_in_current_epoch, + is_active_in_previous_epoch: self.is_active_in_previous_epoch, + current_epoch_effective_balance_gwei: self.current_epoch_effective_balance, + is_current_epoch_attester: self.is_current_epoch_attester, + is_current_epoch_target_attester: self.is_current_epoch_target_attester, + is_previous_epoch_attester: self.is_previous_epoch_attester, + is_previous_epoch_target_attester: self.is_previous_epoch_target_attester, + is_previous_epoch_head_attester: self.is_previous_epoch_head_attester, + } + } +} + +#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)] +pub struct IndividualVotesResponse { + /// The epoch which is considered the "current" epoch. + pub epoch: Epoch, + /// The validators public key. + pub pubkey: PublicKeyBytes, + /// The index of the validator in state.validators. + pub validator_index: Option, + /// Voting statistics for the validator, if they voted in the given epoch. + pub vote: Option, +} + +pub fn post_individual_votes( + req: Request, + beacon_chain: Arc>, +) -> BoxFut { + let response_builder = ResponseBuilder::new(&req); + + let future = req + .into_body() + .concat2() + .map_err(|e| ApiError::ServerError(format!("Unable to get request body: {:?}", e))) + .and_then(|chunks| { + serde_json::from_slice::(&chunks).map_err(|e| { + ApiError::BadRequest(format!( + "Unable to parse JSON into ValidatorDutiesRequest: {:?}", + e + )) + }) + }) + .and_then(move |body| { + let epoch = body.epoch; + + // This is the last slot of the given epoch (one prior to the first slot of the next epoch). + let target_slot = (epoch + 1).start_slot(T::EthSpec::slots_per_epoch()) - 1; + + let (_root, state) = state_at_slot(&beacon_chain, target_slot)?; + let spec = &beacon_chain.spec; + + let mut validator_statuses = ValidatorStatuses::new(&state, spec)?; + validator_statuses.process_attestations(&state, spec)?; + + body.pubkeys + .into_iter() + .map(|pubkey| { + let validator_index_opt = state.get_validator_index(&pubkey).map_err(|e| { + ApiError::ServerError(format!("Unable to read pubkey cache: {:?}", e)) + })?; + + if let Some(validator_index) = validator_index_opt { + let vote = validator_statuses + .statuses + .get(validator_index) + .cloned() + .map(Into::into); + + Ok(IndividualVotesResponse { + epoch, + pubkey, + validator_index: Some(validator_index), + vote, + }) + } else { + Ok(IndividualVotesResponse { + epoch, + pubkey, + validator_index: None, + vote: None, + }) + } + }) + .collect::, _>>() + }) + .and_then(|votes| response_builder?.body_no_ssz(&votes)); + + Box::new(future) +} diff --git a/beacon_node/rest_api/src/lib.rs b/beacon_node/rest_api/src/lib.rs index d72a43998..1473c5e72 100644 --- a/beacon_node/rest_api/src/lib.rs +++ b/beacon_node/rest_api/src/lib.rs @@ -6,6 +6,7 @@ extern crate network as client_network; mod beacon; pub mod config; +mod consensus; mod error; mod helpers; mod metrics; @@ -38,9 +39,12 @@ use tokio::sync::mpsc; use url_query::UrlQuery; pub use crate::helpers::parse_pubkey_bytes; -pub use beacon::{BlockResponse, HeadResponse, StateResponse}; +pub use beacon::{ + BlockResponse, CanonicalHeadResponse, Committee, HeadBeaconBlock, StateResponse, + ValidatorRequest, ValidatorResponse, +}; pub use config::Config; -pub use validator::{BulkValidatorDutiesRequest, ValidatorDuty}; +pub use validator::{ValidatorDutiesRequest, ValidatorDuty}; pub type BoxFut = Box, Error = ApiError> + Send>; pub type NetworkChannel = Arc>>; diff --git a/beacon_node/rest_api/src/router.rs b/beacon_node/rest_api/src/router.rs index 575c936e8..8c30ff3f0 100644 --- a/beacon_node/rest_api/src/router.rs +++ b/beacon_node/rest_api/src/router.rs @@ -1,5 +1,5 @@ use crate::{ - beacon, error::ApiError, helpers, metrics, network, node, spec, validator, BoxFut, + beacon, consensus, error::ApiError, helpers, metrics, network, node, spec, validator, BoxFut, NetworkChannel, }; use beacon_chain::{BeaconChain, BeaconChainTypes}; @@ -74,37 +74,45 @@ pub fn route( (&Method::GET, "/beacon/block_root") => { into_boxfut(beacon::get_block_root::(req, beacon_chain)) } - (&Method::GET, "/beacon/blocks") => { - into_boxfut(helpers::implementation_pending_response(req)) - } (&Method::GET, "/beacon/fork") => into_boxfut(beacon::get_fork::(req, beacon_chain)), - (&Method::GET, "/beacon/attestations") => { - into_boxfut(helpers::implementation_pending_response(req)) - } - (&Method::GET, "/beacon/attestations/pending") => { - into_boxfut(helpers::implementation_pending_response(req)) - } (&Method::GET, "/beacon/genesis_time") => { into_boxfut(beacon::get_genesis_time::(req, beacon_chain)) } - (&Method::GET, "/beacon/validators") => { into_boxfut(beacon::get_validators::(req, beacon_chain)) } - (&Method::GET, "/beacon/validators/indicies") => { - into_boxfut(helpers::implementation_pending_response(req)) + (&Method::POST, "/beacon/validators") => { + into_boxfut(beacon::post_validators::(req, beacon_chain)) } - (&Method::GET, "/beacon/validators/pubkeys") => { - into_boxfut(helpers::implementation_pending_response(req)) + (&Method::GET, "/beacon/validators/all") => { + into_boxfut(beacon::get_all_validators::(req, beacon_chain)) + } + (&Method::GET, "/beacon/validators/active") => { + into_boxfut(beacon::get_active_validators::(req, beacon_chain)) + } + (&Method::GET, "/beacon/state") => { + into_boxfut(beacon::get_state::(req, beacon_chain)) + } + (&Method::GET, "/beacon/state_root") => { + into_boxfut(beacon::get_state_root::(req, beacon_chain)) + } + (&Method::GET, "/beacon/state/genesis") => { + into_boxfut(beacon::get_genesis_state::(req, beacon_chain)) + } + (&Method::GET, "/beacon/committees") => { + into_boxfut(beacon::get_committees::(req, beacon_chain)) } // Methods for Validator - (&Method::GET, "/validator/duties") => { - into_boxfut(validator::get_validator_duties::(req, beacon_chain)) - } (&Method::POST, "/validator/duties") => { validator::post_validator_duties::(req, beacon_chain) } + (&Method::GET, "/validator/duties/all") => { + into_boxfut(validator::get_all_validator_duties::(req, beacon_chain)) + } + (&Method::GET, "/validator/duties/active") => into_boxfut( + validator::get_active_validator_duties::(req, beacon_chain), + ), (&Method::GET, "/validator/block") => { into_boxfut(validator::get_new_beacon_block::(req, beacon_chain, log)) } @@ -118,19 +126,12 @@ pub fn route( validator::publish_attestation::(req, beacon_chain, network_channel, log) } - (&Method::GET, "/beacon/state") => { - into_boxfut(beacon::get_state::(req, beacon_chain)) + (&Method::GET, "/consensus/global_votes") => { + into_boxfut(consensus::get_vote_count::(req, beacon_chain)) } - (&Method::GET, "/beacon/state_root") => { - into_boxfut(beacon::get_state_root::(req, beacon_chain)) + (&Method::POST, "/consensus/individual_votes") => { + consensus::post_individual_votes::(req, beacon_chain) } - (&Method::GET, "/beacon/state/current_finalized_checkpoint") => into_boxfut( - beacon::get_current_finalized_checkpoint::(req, beacon_chain), - ), - (&Method::GET, "/beacon/state/genesis") => { - into_boxfut(beacon::get_genesis_state::(req, beacon_chain)) - } - //TODO: Add aggreggate/filtered state lookups here, e.g. /beacon/validators/balances // Methods for bootstrap and checking configuration (&Method::GET, "/spec") => into_boxfut(spec::get_spec::(req, beacon_chain)), diff --git a/beacon_node/rest_api/src/url_query.rs b/beacon_node/rest_api/src/url_query.rs index 013ddd8e9..10e9878d8 100644 --- a/beacon_node/rest_api/src/url_query.rs +++ b/beacon_node/rest_api/src/url_query.rs @@ -13,11 +13,7 @@ impl<'a> UrlQuery<'a> { /// /// Returns `Err` if `req` does not contain any query parameters. pub fn from_request(req: &'a Request) -> Result { - let query_str = req.uri().query().ok_or_else(|| { - ApiError::BadRequest( - "URL query must be valid and contain at least one key.".to_string(), - ) - })?; + let query_str = req.uri().query().unwrap_or_else(|| ""); Ok(UrlQuery(url::form_urlencoded::parse(query_str.as_bytes()))) } @@ -31,12 +27,21 @@ impl<'a> UrlQuery<'a> { .map(|(key, value)| (key.into_owned(), value.into_owned())) .ok_or_else(|| { ApiError::BadRequest(format!( - "URL query must contain at least one of the following keys: {:?}", + "URL query must be valid and contain at least one of the following keys: {:?}", keys )) }) } + /// Returns the first `(key, value)` pair found where the `key` is in `keys`, if any. + /// + /// Returns `None` if no match is found. + pub fn first_of_opt(mut self, keys: &[&str]) -> Option<(String, String)> { + self.0 + .find(|(key, _value)| keys.contains(&&**key)) + .map(|(key, value)| (key.into_owned(), value.into_owned())) + } + /// Returns the value for `key`, if and only if `key` is the only key present in the query /// parameters. pub fn only_one(self, key: &str) -> Result { diff --git a/beacon_node/rest_api/src/validator.rs b/beacon_node/rest_api/src/validator.rs index 5b3c703cc..3276dd64c 100644 --- a/beacon_node/rest_api/src/validator.rs +++ b/beacon_node/rest_api/src/validator.rs @@ -1,6 +1,5 @@ use crate::helpers::{ - check_content_type_for_json, parse_pubkey_bytes, publish_attestation_to_network, - publish_beacon_block_to_network, + check_content_type_for_json, publish_attestation_to_network, publish_beacon_block_to_network, }; use crate::response_builder::ResponseBuilder; use crate::{ApiError, ApiResult, BoxFut, NetworkChannel, UrlQuery}; @@ -8,20 +7,21 @@ use beacon_chain::{ AttestationProcessingOutcome, BeaconChain, BeaconChainTypes, BlockProcessingOutcome, }; use bls::PublicKeyBytes; -use futures::future::Future; -use futures::stream::Stream; +use futures::{Future, Stream}; use hyper::{Body, Request}; use serde::{Deserialize, Serialize}; use slog::{error, info, warn, Logger}; use ssz_derive::{Decode, Encode}; use std::sync::Arc; use types::beacon_state::EthSpec; -use types::{Attestation, BeaconBlock, CommitteeIndex, Epoch, RelativeEpoch, Slot}; +use types::{Attestation, BeaconBlock, BeaconState, CommitteeIndex, Epoch, RelativeEpoch, Slot}; #[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] pub struct ValidatorDuty { /// The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ pub validator_pubkey: PublicKeyBytes, + /// The validator's index in `state.validators` + pub validator_index: Option, /// The slot at which the validator must attest. pub attestation_slot: Option, /// The index of the committee within `slot` of which the validator is a member. @@ -33,7 +33,7 @@ pub struct ValidatorDuty { } #[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)] -pub struct BulkValidatorDutiesRequest { +pub struct ValidatorDutiesRequest { pub epoch: Epoch, pub pubkeys: Vec, } @@ -52,9 +52,9 @@ pub fn post_validator_duties( .concat2() .map_err(|e| ApiError::ServerError(format!("Unable to get request body: {:?}", e))) .and_then(|chunks| { - serde_json::from_slice::(&chunks).map_err(|e| { + serde_json::from_slice::(&chunks).map_err(|e| { ApiError::BadRequest(format!( - "Unable to parse JSON into BulkValidatorDutiesRequest: {:?}", + "Unable to parse JSON into ValidatorDutiesRequest: {:?}", e )) }) @@ -71,37 +71,61 @@ pub fn post_validator_duties( Box::new(future) } -/// HTTP Handler to retrieve a the duties for a set of validators during a particular epoch -/// -/// The given `epoch` must be within one epoch of the current epoch. -pub fn get_validator_duties( +/// HTTP Handler to retrieve all validator duties for the given epoch. +pub fn get_all_validator_duties( req: Request, beacon_chain: Arc>, ) -> ApiResult { let query = UrlQuery::from_request(&req)?; let epoch = query.epoch()?; - let validator_pubkeys = query - .all_of("validator_pubkeys")? + + let state = get_state_for_epoch(&beacon_chain, epoch)?; + + let validator_pubkeys = state + .validators .iter() - .map(|validator_pubkey_str| parse_pubkey_bytes(validator_pubkey_str)) - .collect::>()?; + .map(|validator| validator.pubkey.clone()) + .collect(); let duties = return_validator_duties(beacon_chain, epoch, validator_pubkeys)?; ResponseBuilder::new(&req)?.body_no_ssz(&duties) } -fn return_validator_duties( +/// HTTP Handler to retrieve all active validator duties for the given epoch. +pub fn get_active_validator_duties( + req: Request, beacon_chain: Arc>, +) -> ApiResult { + let query = UrlQuery::from_request(&req)?; + + let epoch = query.epoch()?; + + let state = get_state_for_epoch(&beacon_chain, epoch)?; + + let validator_pubkeys = state + .validators + .iter() + .filter(|validator| validator.is_active_at(state.current_epoch())) + .map(|validator| validator.pubkey.clone()) + .collect(); + + let duties = return_validator_duties(beacon_chain, epoch, validator_pubkeys)?; + + ResponseBuilder::new(&req)?.body_no_ssz(&duties) +} + +/// Helper function to return the state that can be used to determine the duties for some `epoch`. +pub fn get_state_for_epoch( + beacon_chain: &BeaconChain, epoch: Epoch, - validator_pubkeys: Vec, -) -> Result, ApiError> { +) -> Result, ApiError> { let slots_per_epoch = T::EthSpec::slots_per_epoch(); let head_epoch = beacon_chain.head().beacon_state.current_epoch(); - let mut state = if RelativeEpoch::from_epoch(head_epoch, epoch).is_ok() { - beacon_chain.head().beacon_state + if RelativeEpoch::from_epoch(head_epoch, epoch).is_ok() { + Ok(beacon_chain.head().beacon_state) } else { let slot = if epoch > head_epoch { // Move to the first slot of the epoch prior to the request. @@ -117,8 +141,17 @@ fn return_validator_duties( beacon_chain.state_at_slot(slot).map_err(|e| { ApiError::ServerError(format!("Unable to load state for epoch {}: {:?}", epoch, e)) - })? - }; + }) + } +} + +/// Helper function to get the duties for some `validator_pubkeys` in some `epoch`. +fn return_validator_duties( + beacon_chain: Arc>, + epoch: Epoch, + validator_pubkeys: Vec, +) -> Result, ApiError> { + let mut state = get_state_for_epoch(&beacon_chain, epoch)?; let relative_epoch = RelativeEpoch::from_epoch(state.current_epoch(), epoch) .map_err(|_| ApiError::ServerError(String::from("Loaded state is in the wrong epoch")))?; @@ -173,6 +206,7 @@ fn return_validator_duties( Ok(ValidatorDuty { validator_pubkey, + validator_index: Some(validator_index), attestation_slot: duties.map(|d| d.slot), attestation_committee_index: duties.map(|d| d.index), attestation_committee_position: duties.map(|d| d.committee_position), @@ -181,6 +215,7 @@ fn return_validator_duties( } else { Ok(ValidatorDuty { validator_pubkey, + validator_index: None, attestation_slot: None, attestation_committee_index: None, attestation_committee_position: None, diff --git a/beacon_node/rest_api/tests/test.rs b/beacon_node/rest_api/tests/test.rs index 61d3bbfa3..81f0c3bee 100644 --- a/beacon_node/rest_api/tests/test.rs +++ b/beacon_node/rest_api/tests/test.rs @@ -5,13 +5,15 @@ use node_test_rig::{ environment::{Environment, EnvironmentBuilder}, testing_client_config, ClientConfig, ClientGenesis, LocalBeaconNode, }; -use remote_beacon_node::{PublishStatus, ValidatorDuty}; +use remote_beacon_node::{ + Committee, HeadBeaconBlock, PublishStatus, ValidatorDuty, ValidatorResponse, +}; use std::convert::TryInto; use std::sync::Arc; use tree_hash::TreeHash; use types::{ - test_utils::generate_deterministic_keypair, BeaconBlock, ChainSpec, Domain, Epoch, EthSpec, - MinimalEthSpec, PublicKey, RelativeEpoch, Signature, Slot, + test_utils::generate_deterministic_keypair, BeaconBlock, BeaconState, ChainSpec, Domain, Epoch, + EthSpec, MinimalEthSpec, PublicKey, RelativeEpoch, Signature, Slot, Validator, }; use version; @@ -162,43 +164,6 @@ fn validator_produce_attestation() { ); } -#[test] -fn validator_duties_bulk() { - let mut env = build_env(); - - let spec = &E::default_spec(); - - let node = build_node(&mut env, testing_client_config()); - let remote_node = node.remote_node().expect("should produce remote node"); - - let beacon_chain = node - .client - .beacon_chain() - .expect("client should have beacon chain"); - - let epoch = Epoch::new(0); - - let validators = beacon_chain - .head() - .beacon_state - .validators - .iter() - .map(|v| (&v.pubkey).try_into().expect("pubkey should be valid")) - .collect::>(); - - let duties = env - .runtime() - .block_on( - remote_node - .http - .validator() - .get_duties_bulk(epoch, &validators), - ) - .expect("should fetch duties from http api"); - - check_duties(duties, epoch, validators, beacon_chain, spec); -} - #[test] fn validator_duties() { let mut env = build_env(); @@ -405,6 +370,23 @@ fn validator_block_post() { head.block_root, block_root, "the published block should become the head block" ); + + // Note: this heads check is not super useful for this test, however it is include so it get + // _some_ testing. If you remove this call, make sure it's tested somewhere else. + let heads = env + .runtime() + .block_on(remote_node.http.beacon().get_heads()) + .expect("should get heads"); + + assert_eq!(heads.len(), 1, "there should be only one head"); + assert_eq!( + heads, + vec![HeadBeaconBlock { + beacon_block_root: head.block_root, + beacon_block_slot: head.slot, + }], + "there should be only one head" + ); } #[test] @@ -621,3 +603,194 @@ fn get_version() { assert_eq!(version::version(), version, "result should be as expected"); } + +#[test] +fn get_genesis_state_root() { + let mut env = build_env(); + + let node = build_node(&mut env, testing_client_config()); + let remote_node = node.remote_node().expect("should produce remote node"); + + let slot = Slot::new(0); + + let result = env + .runtime() + .block_on(remote_node.http.beacon().get_state_root(slot)) + .expect("should fetch from http api"); + + let expected = node + .client + .beacon_chain() + .expect("should have beacon chain") + .rev_iter_state_roots() + .find(|(_cur_root, cur_slot)| slot == *cur_slot) + .map(|(cur_root, _)| cur_root) + .expect("chain should have state root at slot"); + + assert_eq!(result, expected, "result should be as expected"); +} + +#[test] +fn get_genesis_block_root() { + let mut env = build_env(); + + let node = build_node(&mut env, testing_client_config()); + let remote_node = node.remote_node().expect("should produce remote node"); + + let slot = Slot::new(0); + + let result = env + .runtime() + .block_on(remote_node.http.beacon().get_block_root(slot)) + .expect("should fetch from http api"); + + let expected = node + .client + .beacon_chain() + .expect("should have beacon chain") + .rev_iter_block_roots() + .find(|(_cur_root, cur_slot)| slot == *cur_slot) + .map(|(cur_root, _)| cur_root) + .expect("chain should have state root at slot"); + + assert_eq!(result, expected, "result should be as expected"); +} + +#[test] +fn get_validators() { + let mut env = build_env(); + + let node = build_node(&mut env, testing_client_config()); + let remote_node = node.remote_node().expect("should produce remote node"); + let chain = node + .client + .beacon_chain() + .expect("node should have beacon chain"); + let state = &chain.head().beacon_state; + + let validators = state.validators.iter().take(2).collect::>(); + let pubkeys = validators + .iter() + .map(|v| (&v.pubkey).try_into().expect("should decode pubkey bytes")) + .collect(); + + let result = env + .runtime() + .block_on(remote_node.http.beacon().get_validators(pubkeys, None)) + .expect("should fetch from http api"); + + result + .iter() + .zip(validators.iter()) + .for_each(|(response, validator)| compare_validator_response(state, response, validator)); +} + +#[test] +fn get_all_validators() { + let mut env = build_env(); + + let node = build_node(&mut env, testing_client_config()); + let remote_node = node.remote_node().expect("should produce remote node"); + let chain = node + .client + .beacon_chain() + .expect("node should have beacon chain"); + let state = &chain.head().beacon_state; + + let result = env + .runtime() + .block_on(remote_node.http.beacon().get_all_validators(None)) + .expect("should fetch from http api"); + + result + .iter() + .zip(state.validators.iter()) + .for_each(|(response, validator)| compare_validator_response(state, response, validator)); +} + +#[test] +fn get_active_validators() { + let mut env = build_env(); + + let node = build_node(&mut env, testing_client_config()); + let remote_node = node.remote_node().expect("should produce remote node"); + let chain = node + .client + .beacon_chain() + .expect("node should have beacon chain"); + let state = &chain.head().beacon_state; + + let result = env + .runtime() + .block_on(remote_node.http.beacon().get_active_validators(None)) + .expect("should fetch from http api"); + + /* + * This test isn't comprehensive because all of the validators in the state are active (i.e., + * there is no one to exclude. + * + * This should be fixed once we can generate more interesting scenarios with the + * `NodeTestRig`. + */ + + let validators = state + .validators + .iter() + .filter(|validator| validator.is_active_at(state.current_epoch())); + + result + .iter() + .zip(validators) + .for_each(|(response, validator)| compare_validator_response(state, response, validator)); +} + +#[test] +fn get_committees() { + let mut env = build_env(); + + let node = build_node(&mut env, testing_client_config()); + let remote_node = node.remote_node().expect("should produce remote node"); + let chain = node + .client + .beacon_chain() + .expect("node should have beacon chain"); + + let epoch = Epoch::new(0); + + let result = env + .runtime() + .block_on(remote_node.http.beacon().get_committees(epoch)) + .expect("should fetch from http api"); + + let expected = chain + .head() + .beacon_state + .get_beacon_committees_at_epoch(RelativeEpoch::Current) + .expect("should get committees") + .iter() + .map(|c| Committee { + slot: c.slot, + index: c.index, + committee: c.committee.to_vec(), + }) + .collect::>(); + + assert_eq!(result, expected, "result should be as expected"); +} + +fn compare_validator_response( + state: &BeaconState, + response: &ValidatorResponse, + validator: &Validator, +) { + let response_validator = response.validator.clone().expect("should have validator"); + let i = response + .validator_index + .expect("should have validator index"); + let balance = response.balance.expect("should have balance"); + + assert_eq!(response.pubkey, validator.pubkey, "pubkey"); + assert_eq!(response_validator, *validator, "validator"); + assert_eq!(state.balances[i], balance, "balances"); + assert_eq!(state.validators[i], *validator, "validator index"); +} diff --git a/book/book.toml b/book/book.toml index f48235fc9..df47f4d7b 100644 --- a/book/book.toml +++ b/book/book.toml @@ -4,3 +4,6 @@ language = "en" multilingual = false src = "src" title = "Lighthouse Book" + +[output.html] +additional-css =["src/css/custom.css"] diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index d547594da..096a57270 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -9,6 +9,10 @@ * [Simple Local Testnet](./simple-testnet.md) * [API](./api.md) * [HTTP (RESTful JSON)](./http.md) + * [/beacon](./http_beacon.md) + * [/validator](./http_validator.md) + * [/consensus](./http_consensus.md) + * [/network](./http_network.md) * [WebSocket](./websockets.md) * [Contributing](./contributing.md) * [Development Environment](./setup.md) diff --git a/book/src/css/custom.css b/book/src/css/custom.css new file mode 100644 index 000000000..5828fd62d --- /dev/null +++ b/book/src/css/custom.css @@ -0,0 +1,4 @@ +table { + margin: 0; + border-collapse: collapse; +} diff --git a/book/src/http.md b/book/src/http.md index 918f349b0..deef0088c 100644 --- a/book/src/http.md +++ b/book/src/http.md @@ -1,85 +1,39 @@ # HTTP API -[![Swagger Badge]][Swagger Link] - -[Swagger Badge]: https://img.shields.io/badge/Open%20API-0.2.0-success -[Swagger Link]: https://app.swaggerhub.com/apis-docs/spble/lighthouse_rest_api/0.2.0 - - -**The Lighthouse API is documented in Open API format and is available at -[SwaggerHub: Lighthouse REST -API](https://app.swaggerhub.com/apis-docs/spble/lighthouse_rest_api/0.2.0).** - -By default, a Lighthouse `beacon_node` exposes a HTTP server on `localhost:5052`. +A Lighthouse beacon node can be configured to expose a HTTP server by supplying the `--http` flag. The default listen address is `localhost:5052`. The following CLI flags control the HTTP server: -- `--no-api`: disable the HTTP server. -- `--api-port`: specify the listen port of the server. -- `--api-address`: specify the listen address of the server. +- `--http`: enable the HTTP server (required even if the following flags are + provided). +- `--http-port`: specify the listen port of the server. +- `--http-address`: specify the listen address of the server. -## Examples +The API is logically divided into several core endpoints, each documented in +detail: -In addition to the complete Open API docs (see above), some examples are -provided below. +Endpoint | Description | +| --- | -- | +[`/beacon`](./http_beacon.md) | General information about the beacon chain. +[`/validator`](./http_validator.md) | Provides functionality to validator clients. +[`/consensus`](./http_consensus.md) | Proof-of-stake voting statistics. +[`/network`](./http_network.md) | Information about the p2p network. -_Examples assume there is a Lighthouse node exposing a HTTP API on -`localhost:5052`. Responses are JSON._ +_Please note: The OpenAPI format at +[SwaggerHub: Lighthouse REST +API](https://app.swaggerhub.com/apis-docs/spble/lighthouse_rest_api/0.2.0) has +been **deprecated**. This documentation is now the source of truth for the REST API._ -### Get the node's beacon chain head +## Troubleshooting -```bash -$ curl localhost:5052/beacon/head +### HTTP API is unavailable or refusing connections + +Ensure the `--http` flag has been supplied at the CLI. + +You can quickly check that the HTTP endpoint is up using `curl`: -{"slot":0,"block_root":"0x827bf71805540aa13f6d8c7d18b41b287b2094a4d7a28cbb8deb061dbf5df4f5","state_root":"0x90a78d73294bc9c7519a64e1912161be0e823eb472012ff54204e15a4d717fa5"}% ``` +curl "localhost:5052/beacon/head" -### Get the node's finalized checkpoint - -```bash -$ curl localhost:5052/beacon/latest_finalized_checkpoint - -{"epoch":0,"root":"0x0000000000000000000000000000000000000000000000000000000000000000"}% -``` - -### Get the node's ENR - -```bash -$ curl localhost:5052/network/enr - -"-IW4QFyf1VlY5pZs0xZuvKMRZ9_cdl9WMCDAAJXZiZiuGcfRYoU40VPrYDLQj5prneJIz3zcbTjHp9BbThc-yiymJO8HgmlwhH8AAAGDdGNwgiMog3VkcIIjKIlzZWNwMjU2azGhAjg0-DsTkQynhJCRnLLttBK1RS78lmUkLa-wgzAi-Ob5"% -``` - -### Get a list of connected peer ids - -```bash -$ curl localhost:5052/network/peers - -["QmeMFRTWfo3KbVG7dEBXGhyRMa29yfmnJBXW84rKuGEhuL"]% -``` - -### Get the node's peer id - -```bash -$ curl localhost:5052/network/peer_id - -"QmRD1qs2AqNNRdBcGHUGpUGkpih5cmdL32mhh22Sy79xsJ"% -``` - -### Get the list of listening libp2p addresses - -Lists all the libp2p multiaddrs that the node is listening on. - -```bash -$ curl localhost:5052/network/listen_addresses - -["/ip4/127.0.0.1/tcp/9000","/ip4/192.168.1.121/tcp/9000","/ip4/172.17.0.1/tcp/9000","/ip4/172.42.0.1/tcp/9000","/ip6/::1/tcp/9000","/ip6/fdd3:c293:1bc::203/tcp/9000","/ip6/fdd3:c293:1bc:0:9aa9:b2ea:c610:44db/tcp/9000"]% -``` - -### Pretty-print the genesis state and state root - -Returns the genesis state and state root in your terminal, in YAML. - -```bash -$ curl --header "Content-Type: application/yaml" "localhost:5052/beacon/state?slot=0" +{"slot":37934,"block_root":"0x4d3ae7ebe8c6ef042db05958ec76e8f7be9d412a67a0defa6420a677249afdc7","state_root":"0x1c86b13ffc70a41e410eccce20d33f1fe59d148585ea27c2afb4060f75fe6be2","finalized_slot":37856,"finalized_block_root":"0xbdae152b62acef1e5c332697567d2b89e358628790b8273729096da670b23e86","justified_slot":37888,"justified_block_root":"0x01c2f516a407d8fdda23cad4ed4381e4ab8913d638f935a2fe9bd00d6ced5ec4","previous_justified_slot":37856,"previous_justified_block_root":"0xbdae152b62acef1e5c332697567d2b89e358628790b8273729096da670b23e86"} ``` diff --git a/book/src/http_beacon.md b/book/src/http_beacon.md new file mode 100644 index 000000000..c926bdb25 --- /dev/null +++ b/book/src/http_beacon.md @@ -0,0 +1,431 @@ +# Lighthouse REST API: `/beacon` + +The `/beacon` endpoints provide information about the canonical head of the +beacon chain and also historical information about beacon blocks and states. + +## Endpoints + +HTTP Path | Description | +| --- | -- | +[`/beacon/head`](#beaconhead) | Info about the block at the head of the chain. +[`/beacon/heads`](#beaconheads) | Returns a list of all known chain heads. +[`/beacon/block_root`](#beaconblock_root) | Resolve a slot to a block root. +[`/beacon/block`](#beaconblock) | Get a `BeaconBlock` by slot or root. +[`/beacon/state_root`](#beaconstate_root) | Resolve a slot to a state root. +[`/beacon/state`](#beaconstate) | Get a `BeaconState` by slot or root. +[`/beacon/validators`](#beaconvalidators) | Query for one or more validators. +[`/beacon/validators/all`](#beaconvalidatorsall) | Get all validators. +[`/beacon/validators/active`](#beaconvalidatorsactive) | Get all active validators. +[`/beacon/committees`](#beaconcommittees) | Get the shuffling for an epoch. + +## `/beacon/head` + +Requests information about the head of the beacon chain, from the node's +perspective. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/beacon/head` +Method | GET +JSON Encoding | Object +Query Parameters | None +Typical Responses | 200 + +### Example Response + +```json +{ + "slot": 37923, + "block_root": "0xe865d4805395a0776b8abe46d714a9e64914ab8dc5ff66624e5a1776bcc1684b", + "state_root": "0xe500e3567ab273c9a6f8a057440deff476ab236f0983da27f201ee9494a879f0", + "finalized_slot": 37856, + "finalized_block_root": "0xbdae152b62acef1e5c332697567d2b89e358628790b8273729096da670b23e86", + "justified_slot": 37888, + "justified_block_root": "0x01c2f516a407d8fdda23cad4ed4381e4ab8913d638f935a2fe9bd00d6ced5ec4", + "previous_justified_slot": 37856, + "previous_justified_block_root": "0xbdae152b62acef1e5c332697567d2b89e358628790b8273729096da670b23e86" +} +``` + +## `/beacon/heads` + +Returns the roots of all known head blocks. Only one of these roots is the +canonical head and that is decided by the fork choice algorithm. See [`/beacon/head`](#beaconhead) for the canonical head. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/beacon/heads` +Method | GET +JSON Encoding | Object +Query Parameters | None +Typical Responses | 200 + +### Example Response + +```json +[ + { + "beacon_block_root": "0x226b2fd7c5f3d31dbb21444b96dfafe715f0017cd16545ecc4ffa87229496a69", + "beacon_block_slot": 38373 + }, + { + "beacon_block_root": "0x41ed5b253c4fc841cba8a6d44acbe101866bc674c3cfa3c4e9f7388f465aa15b", + "beacon_block_slot": 38375 + } +] +``` + +## `/beacon/block_root` + +Returns the block root for the given slot in the canonical chain. If there +is a re-org, the same slot may return a different root. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/beacon/block_root` +Method | GET +JSON Encoding | Object +Query Parameters | `slot` +Typical Responses | 200, 404 + +## Parameters + +- `slot` (`Slot`): the slot to be resolved to a root. + +### Example Response + +```json +"0xc35ddf4e71c31774e0594bd7eb32dfe50b54dbc40abd594944254b4ec8895196" +``` + +## `/beacon/block` + +Request that the node return a beacon chain block that matches the provided +criteria (a block `root` or beacon chain `slot`). Only one of the parameters +should be provided as a criteria. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/beacon/block` +Method | GET +JSON Encoding | Object +Query Parameters | `slot`, `root` +Typical Responses | 200, 404 + +### Parameters + +Accepts **only one** of the following parameters: + +- `slot` (`Slot`): Query by slot number. Any block returned must be in the canonical chain (i.e., +either the head or an ancestor of the head). +- `root` (`Bytes32`): Query by tree hash root. A returned block is not required to be in the +canonical chain. + +### Returns + +Returns an object containing a single [`BeaconBlock`](https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/core/0_beacon-chain.md#beaconblock) and its signed root. + +### Example Response + +```json +{ + "root": "0xc35ddf4e71c31774e0594bd7eb32dfe50b54dbc40abd594944254b4ec8895196", + "beacon_block": { + "slot": 0, + "parent_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "state_root": "0xf15690b6be4ed42ea1ee0741eb4bfd4619d37be8229b84b4ddd480fb028dcc8f", + "body": { + "randao_reveal": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "eth1_data": { + "deposit_root": "0x0000000000000000000000000000000000000000000000000000000000000000", + "deposit_count": 0, + "block_hash": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "graffiti": "0x0000000000000000000000000000000000000000000000000000000000000000", + "proposer_slashings": [], + "attester_slashings": [], + "attestations": [], + "deposits": [], + "voluntary_exits": [] + }, + "signature": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + } +} +``` + +## `/beacon/state_root` + +Returns the state root for the given slot in the canonical chain. If there +is a re-org, the same slot may return a different root. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/beacon/state_root` +Method | GET +JSON Encoding | Object +Query Parameters | `slot` +Typical Responses | 200, 404 + +## Parameters + +- `slot` (`Slot`): the slot to be resolved to a root. + +### Example Response + +```json +"0xf15690b6be4ed42ea1ee0741eb4bfd4619d37be8229b84b4ddd480fb028dcc8f" +``` + +## `/beacon/state` + +Request that the node return a beacon chain state that matches the provided +criteria (a state `root` or beacon chain `slot`). Only one of the parameters +should be provided as a criteria. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/beacon/state` +Method | GET +JSON Encoding | Object +Query Parameters | `slot`, `root` +Typical Responses | 200, 404 + +### Parameters + +Accepts **only one** of the following parameters: + +- `slot` (`Slot`): Query by slot number. Any state returned must be in the canonical chain (i.e., +either the head or an ancestor of the head). +- `root` (`Bytes32`): Query by tree hash root. A returned state is not required to be in the +canonical chain. + +### Returns + +Returns an object containing a single +[`BeaconState`](https://github.com/ethereum/eth2.0-specs/blob/v0.9.2/specs/core/0_beacon-chain.md#beaconstate) +and its tree hash root. + +### Example Response + +```json +{ + "root": "0x528e54ca5d4c957729a73f40fc513ae312e054c7295775c4a2b21f423416a72b", + "beacon_state": { + "genesis_time": 1575652800, + "slot": 18478 + } +} +``` + +_Truncated for brevity._ + +## `/beacon/validators` + +Request that the node returns information about one or more validator public +keys. This request takes the form of a `POST` request to allow sending a large +number of pubkeys in the request. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/beacon/validators` +Method | POST +JSON Encoding | Object +Query Parameters | None +Typical Responses | 200 + +### Request Body + +Expects the following object in the POST request body: + +``` +{ + state_root: Bytes32, + pubkeys: [PublicKey] +} +``` + +The `state_root` field indicates which `BeaconState` should be used to collect +the information. The `state_root` is optional and omitting it will result in +the canonical head state being used. + + +### Returns + +Returns an object describing several aspects of the given validator. + +### Example + +### Request Body + +```json +{ + "pubkeys": [ + "0x98f87bc7c8fa10408425bbeeeb3dc387e3e0b4bd92f57775b60b39156a16f9ec80b273a64269332d97bdb7d93ae05a16", + "0x42f87bc7c8fa10408425bbeeeb3dc3874242b4bd92f57775b60b39142426f9ec80b273a64269332d97bdb7d93ae05a42" + ] +} +``` + +_Note: for demonstration purposes the second pubkey is some unknown pubkey._ + +### Response Body + +```json +[ + { + "pubkey": "0x98f87bc7c8fa10408425bbeeeb3dc387e3e0b4bd92f57775b60b39156a16f9ec80b273a64269332d97bdb7d93ae05a16", + "validator_index": 14935, + "balance": 3228885987, + "validator": { + "pubkey": "0x98f87bc7c8fa10408425bbeeeb3dc387e3e0b4bd92f57775b60b39156a16f9ec80b273a64269332d97bdb7d93ae05a16", + "withdrawal_credentials": "0x00b7bec22d5bda6b2cca1343d4f640d0e9ccc204a06a73703605c590d4c0d28e", + "effective_balance": 3200000000, + "slashed": false, + "activation_eligibility_epoch": 0, + "activation_epoch": 0, + "exit_epoch": 18446744073709551615, + "withdrawable_epoch": 18446744073709551615 + } + }, + { + "pubkey": "0x42f87bc7c8fa10408425bbeeeb3dc3874242b4bd92f57775b60b39142426f9ec80b273a64269332d97bdb7d93ae05a42", + "validator_index": null, + "balance": null, + "validator": null + } +] +``` + +## `/beacon/validators/all` + +Returns all validators. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/beacon/validators/all` +Method | GET +JSON Encoding | Object +Query Parameters | `state_root` (optional) +Typical Responses | 200 + +### Parameters + +The optional `state_root` (`Bytes32`) query parameter indicates which +`BeaconState` should be used to collect the information. When omitted, the +canonical head state will be used. + +### Returns + +The return format is identical to the [`/beacon/validators`](#beaconvalidators) response body. + +## `/beacon/validators/active` + +Returns all validators that are active in the state defined by `state_root`. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/beacon/validators/active` +Method | GET +JSON Encoding | Object +Query Parameters | `state_root` (optional) +Typical Responses | 200 + +### Parameters + +The optional `state_root` (`Bytes32`) query parameter indicates which +`BeaconState` should be used to collect the information. When omitted, the +canonical head state will be used. + +### Returns + +The return format is identical to the [`/beacon/validators`](#beaconvalidators) response body. + +## `/beacon/committees` + +Request the committees (a.k.a. "shuffling") for all slots and committee indices +in a given `epoch`. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/beacon/committees` +Method | GET +JSON Encoding | Object +Query Parameters | `epoch` +Typical Responses | 200 + +### Parameters + +The `epoch` (`Epoch`) query parameter is required and defines the epoch for +which the committees will be returned. All slots contained within the response will +be inside this epoch. + +### Returns + +A list of beacon committees. + +### Example Response + +```json +[ + { + "slot": 4768, + "index": 0, + "committee": [ + 1154, + 492, + 9667, + 3089, + 8987, + 1421, + 224, + 11243, + 2127, + 2329, + 188, + 482, + 486 + ] + }, + { + "slot": 4768, + "index": 1, + "committee": [ + 5929, + 8482, + 5528, + 6130, + 14343, + 9777, + 10808, + 12739, + 15234, + 12819, + 5423, + 6320, + 9991 + ] + } +] +``` + +_Truncated for brevity._ diff --git a/book/src/http_consensus.md b/book/src/http_consensus.md new file mode 100644 index 000000000..c71b78ce3 --- /dev/null +++ b/book/src/http_consensus.md @@ -0,0 +1,189 @@ +# Lighthouse REST API: `/consensus` + +The `/consensus` endpoints provide information on results of the proof-of-stake +voting process used for finality/justification under Casper FFG. + +## Endpoints + +HTTP Path | Description | +| --- | -- | +[`/consensus/global_votes`](#consensusglobal_votes) | A global vote count for a given epoch. +[`/consensus/individual_votes`](#consensusindividual_votes) | A per-validator breakdown of votes in a given epoch. + +## `/consensus/global_votes` + +Returns a global count of votes for some given `epoch`. The results are included +both for the current and previous (`epoch - 1`) epochs since both are required +by the beacon node whilst performing per-epoch-processing. + +Generally, you should consider the "current" values to be incomplete and the +"previous" values to be final. This is because validators can continue to +include attestations from the _current_ epoch in the _next_ epoch, however this +is not the case for attestations from the _previous_ epoch. + +``` + `epoch` query parameter + | + | --------- values are calcuated here + | | + v v +Epoch: |---previous---|---current---|---next---| + + |-------------| + ^ + | + window for including "current" attestations + in a block +``` + +The votes are expressed in terms of staked _effective_ `Gwei` (i.e., not the number of +individual validators). For example, if a validator has 32 ETH staked they will +increase the `current_epoch_attesting_gwei` figure by `32,000,000,000` if they +have an attestation included in a block during the current epoch. If this +validator has more than 32 ETH, that extra ETH will not count towards their +vote (that is why it is _effective_ `Gwei`). + +The following fields are returned: + +- `current_epoch_active_gwei`: the total staked gwei that was active (i.e., + able to vote) during the current epoch. +- `current_epoch_attesting_gwei`: the total staked gwei that had one or more + attestations included in a block during the current epoch (multiple + attestations by the same validator do not increase this figure). +- `current_epoch_target_attesting_gwei`: the total staked gwei that attested to + the majority-elected Casper FFG target epoch during the current epoch. This + figure must be equal to or less than `current_epoch_attesting_gwei`. +- `previous_epoch_active_gwei`: as above, but during the previous epoch. +- `previous_epoch_attesting_gwei`: see `current_epoch_attesting_gwei`. +- `previous_epoch_target_attesting_gwei`: see `current_epoch_target_attesting_gwei`. +- `previous_epoch_head_attesting_gwei`: the total staked gwei that attested to a + head beacon block that is in the canonical chain. + +From this data you can calculate some interesting figures: + +#### Participation Rate + +`previous_epoch_attesting_gwei / previous_epoch_active_gwei` + +Expresses the ratio of validators that managed to have an attestation +voting upon the previous epoch included in a block. + +#### Justification/Finalization Rate + +`previous_epoch_target_attesting_gwei / previous_epoch_active_gwei` + +When this value is greater than or equal to `2/3` it is possible that the +beacon chain may justify and/or finalize the epoch. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/consensus/global_votes` +Method | GET +JSON Encoding | Object +Query Parameters | `epoch` +Typical Responses | 200 + +### Parameters + +Requires the `epoch` (`Epoch`) query parameter to determine which epoch will be +considered the current epoch. + +### Returns + +A report on global validator voting participation. + +### Example + +```json +{ + "current_epoch_active_gwei": 52377600000000, + "previous_epoch_active_gwei": 52377600000000, + "current_epoch_attesting_gwei": 50740900000000, + "current_epoch_target_attesting_gwei": 49526000000000, + "previous_epoch_attesting_gwei": 52377600000000, + "previous_epoch_target_attesting_gwei": 51063400000000, + "previous_epoch_head_attesting_gwei": 9248600000000 +} +``` + +## `/consensus/individual_votes` + +Returns a per-validator summary of how that validator performed during the +current epoch. + +The [Global Votes](#consensusglobal_votes) endpoint is the summation of all of these +individual values, please see it for definitions of terms like "current_epoch", +"previous_epoch" and "target_attester". + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/consensus/individual_votes` +Method | POST +JSON Encoding | Object +Query Parameters | None +Typical Responses | 200 + +### Request Body + +Expects the following object in the POST request body: + +``` +{ + epoch: Epoch, + pubkeys: [PublicKey] +} +``` + +### Returns + +A report on the validators voting participation. + +### Example + +#### Request Body + +```json +{ + "epoch": 1203, + "pubkeys": [ + "0x98f87bc7c8fa10408425bbeeeb3dc387e3e0b4bd92f57775b60b39156a16f9ec80b273a64269332d97bdb7d93ae05a16", + "0x42f87bc7c8fa10408425bbeeeb3dc3874242b4bd92f57775b60b39142426f9ec80b273a64269332d97bdb7d93ae05a42" + ] +} +``` + +_Note: for demonstration purposes the second pubkey is some unknown pubkey._ + +#### Response Body + +```json +[ + { + "epoch": 1203, + "pubkey": "0x98f87bc7c8fa10408425bbeeeb3dc387e3e0b4bd92f57775b60b39156a16f9ec80b273a64269332d97bdb7d93ae05a16", + "validator_index": 14935, + "vote": { + "is_slashed": false, + "is_withdrawable_in_current_epoch": false, + "is_active_in_current_epoch": true, + "is_active_in_previous_epoch": true, + "current_epoch_effective_balance_gwei": 3200000000, + "is_current_epoch_attester": true, + "is_current_epoch_target_attester": true, + "is_previous_epoch_attester": true, + "is_previous_epoch_target_attester": true, + "is_previous_epoch_head_attester": false + } + }, + { + "epoch": 1203, + "pubkey": "0x42f87bc7c8fa10408425bbeeeb3dc3874242b4bd92f57775b60b39142426f9ec80b273a64269332d97bdb7d93ae05a42", + "validator_index": null, + "vote": null + } +] +``` diff --git a/book/src/http_network.md b/book/src/http_network.md new file mode 100644 index 000000000..2f1d6e9b2 --- /dev/null +++ b/book/src/http_network.md @@ -0,0 +1,75 @@ +# Lighthouse REST API: `/network` + +The `/network` endpoints provide information about the p2p network that +Lighthouse uses to communicate with other beacon nodes. + +## Endpoints + +HTTP Path | Description | +| --- | -- | +[`/network/peer_id`](#networkpeer_id) | Get a node's libp2p `PeerId`. +[`/network/peers`](#networkpeers) | List a node's libp2p peers (as `PeerIds`). +[`/network/enr`](#networkenr) | Get a node's discovery `ENR` address. + +## `/network/peer_id` + +Requests the beacon node's local `PeerId`. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/network/peer_id` +Method | GET +JSON Encoding | String (base58) +Query Parameters | None +Typical Responses | 200 + +### Example Response + +```json +"QmVFcULBYZecPdCKgGmpEYDqJLqvMecfhJadVBtB371Avd" +``` + +## `/network/peers` + +Requests one `MultiAddr` for each peer connected to the beacon node. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/network/peers` +Method | GET +JSON Encoding | [String] (base58) +Query Parameters | None +Typical Responses | 200 + +### Example Response + +```json +[ + "QmaPGeXcfKFMU13d8VgbnnpeTxcvoFoD9bUpnRGMUJ1L9w", + "QmZt47cP8V96MgiS35WzHKpPbKVBMqr1eoBNTLhQPqpP3m" +] +``` + +## `network/enr` + +Requests the beacon node for its listening `ENR` address. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/network/enr` +Method | GET +JSON Encoding | String (base64) +Query Parameters | None +Typical Responses | 200 + +### Example Response + +```json +"-IW4QPYyGkXJSuJ2Eji8b-m4PTNrW4YMdBsNOBrYAdCk8NLMJcddAiQlpcv6G_hdNjiLACOPTkqTBhUjnC0wtIIhyQkEgmlwhKwqAPqDdGNwgiMog3VkcIIjKIlzZWNwMjU2azGhA1sBKo0yCfw4Z_jbggwflNfftjwKACu-a-CoFAQHJnrm" +``` diff --git a/book/src/http_validator.md b/book/src/http_validator.md new file mode 100644 index 000000000..413a8279b --- /dev/null +++ b/book/src/http_validator.md @@ -0,0 +1,153 @@ +# Lighthouse REST API: `/validator` + +The `/validator` endpoints provide the minimum functionality required for a validator +client to connect to the beacon node and produce blocks and attestations. + +## Endpoints + +HTTP Path | Description | +| --- | -- | +[`/validator/duties`](#validatorduties) | Provides block and attestation production information for validators. +[`/validator/duties/all`](#validatordutiesall) | Provides block and attestation production information for all validators. +[`/validator/duties/active`](#validatordutiesactive) | Provides block and attestation production information for all active validators. + +## `/validator/duties` + +Request information about when a validator must produce blocks and attestations +at some given `epoch`. The information returned always refers to the canonical +chain and the same input parameters may yield different results after a re-org. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/validator/duties` +Method | POST +JSON Encoding | Object +Query Parameters | None +Typical Responses | 200 + +### Request Body + +Expects the following object in the POST request body: + +``` +{ + epoch: Epoch, + pubkeys: [PublicKey] +} +``` + +Duties are assigned on a per-epoch basis, all duties returned will contain +slots that are inside the given `epoch`. A set of duties will be returned for +each of the `pubkeys`. + +Validators who are not known to the beacon chain (e.g., have not yet deposited) +will have `null` values for most fields. + + +### Returns + +A set of duties for each given pubkey. + +### Example + +#### Request Body + +```json +{ + "epoch": 1203, + "pubkeys": [ + "0x98f87bc7c8fa10408425bbeeeb3dc387e3e0b4bd92f57775b60b39156a16f9ec80b273a64269332d97bdb7d93ae05a16", + "0x42f87bc7c8fa10408425bbeeeb3dc3874242b4bd92f57775b60b39142426f9ec80b273a64269332d97bdb7d93ae05a42" + ] +} +``` + +_Note: for demonstration purposes the second pubkey is some unknown pubkey._ + +#### Response Body + +```json +[ + { + "validator_pubkey": "0x98f87bc7c8fa10408425bbeeeb3dc387e3e0b4bd92f57775b60b39156a16f9ec80b273a64269332d97bdb7d93ae05a16", + "validator_index": 14935, + "attestation_slot": 38511, + "attestation_committee_index": 3, + "attestation_committee_position": 39, + "block_proposal_slots": [] + }, + { + "validator_pubkey": "0x42f87bc7c8fa10408425bbeeeb3dc3874242b4bd92f57775b60b39142426f9ec80b273a64269332d97bdb7d93ae05a42", + "validator_index": null, + "attestation_slot": null, + "attestation_committee_index": null, + "attestation_committee_position": null, + "block_proposal_slots": [] + } +] +``` + +## `/validator/duties/all` + +Returns the duties for all validators, equivalent to calling [Validator +Duties](#validator-duties) while providing all known validator public keys. + +Considering that duties for non-active validators will just be `null`, it is +generally more efficient to query using [Active Validator +Duties](#active-validator-duties). + +This endpoint will only return validators that were in the beacon state +in the given epoch. For example, if the query epoch is 10 and some validator +deposit was included in epoch 11, that validator will not be included in the +result. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/validator/duties/all` +Method | GET +JSON Encoding | Object +Query Parameters | `epoch` +Typical Responses | 200 + +### Parameters + +The duties returned will all be inside the given `epoch` (`Epoch`) query +parameter. This parameter is required. + +### Returns + +The return format is identical to the [Validator Duties](#validator-duties) response body. + +## `/validator/duties/active` + +Returns the duties for all active validators, equivalent to calling [Validator +Duties](#validator-duties) while providing all known validator public keys that +are active in the given epoch. + +This endpoint will only return validators that were in the beacon state +in the given epoch. For example, if the query epoch is 10 and some validator +deposit was included in epoch 11, that validator will not be included in the +result. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/validator/duties/active` +Method | GET +JSON Encoding | Object +Query Parameters | `epoch` +Typical Responses | 200 + +### Parameters + +The duties returned will all be inside the given `epoch` (`Epoch`) query +parameter. This parameter is required. + +### Returns + +The return format is identical to the [Validator Duties](#validator-duties) response body. diff --git a/eth2/state_processing/src/per_epoch_processing.rs b/eth2/state_processing/src/per_epoch_processing.rs index 080767203..f2c7e4135 100644 --- a/eth2/state_processing/src/per_epoch_processing.rs +++ b/eth2/state_processing/src/per_epoch_processing.rs @@ -1,7 +1,6 @@ use errors::EpochProcessingError as Error; use tree_hash::TreeHash; use types::*; -use validator_statuses::{TotalBalances, ValidatorStatuses}; pub mod apply_rewards; pub mod errors; @@ -13,6 +12,7 @@ pub mod validator_statuses; pub use apply_rewards::process_rewards_and_penalties; pub use process_slashings::process_slashings; pub use registry_updates::process_registry_updates; +pub use validator_statuses::{TotalBalances, ValidatorStatus, ValidatorStatuses}; /// Performs per-epoch processing on some BeaconState. /// diff --git a/eth2/types/src/beacon_state.rs b/eth2/types/src/beacon_state.rs index 4e31d9266..a91b26a7a 100644 --- a/eth2/types/src/beacon_state.rs +++ b/eth2/types/src/beacon_state.rs @@ -388,6 +388,19 @@ impl BeaconState { cache.get_beacon_committees_at_slot(slot) } + /// Get all of the Beacon committees at a given relative epoch. + /// + /// Utilises the committee cache. + /// + /// Spec v0.9.1 + pub fn get_beacon_committees_at_epoch( + &self, + relative_epoch: RelativeEpoch, + ) -> Result, Error> { + let cache = self.committee_cache(relative_epoch)?; + cache.get_all_beacon_committees() + } + /// Compute the proposer (not necessarily for the Beacon chain) from a list of indices. /// /// Spec v0.9.1 diff --git a/eth2/types/src/beacon_state/committee_cache.rs b/eth2/types/src/beacon_state/committee_cache.rs index 54c6853ba..78b2af3fc 100644 --- a/eth2/types/src/beacon_state/committee_cache.rs +++ b/eth2/types/src/beacon_state/committee_cache.rs @@ -141,6 +141,21 @@ impl CommitteeCache { .collect() } + /// Returns all committees for `self.initialized_epoch`. + pub fn get_all_beacon_committees(&self) -> Result, Error> { + let initialized_epoch = self + .initialized_epoch + .ok_or_else(|| Error::CommitteeCacheUninitialized(None))?; + + initialized_epoch.slot_iter(self.slots_per_epoch).try_fold( + Vec::with_capacity(self.slots_per_epoch as usize), + |mut vec, slot| { + vec.append(&mut self.get_beacon_committees_at_slot(slot)?); + Ok(vec) + }, + ) + } + /// Returns the `AttestationDuty` for the given `validator_index`. /// /// Returns `None` if the `validator_index` does not exist, does not have duties or `Self` is diff --git a/eth2/utils/bls/src/aggregate_signature.rs b/eth2/utils/bls/src/aggregate_signature.rs index 5a081c943..146fbfd5d 100644 --- a/eth2/utils/bls/src/aggregate_signature.rs +++ b/eth2/utils/bls/src/aggregate_signature.rs @@ -5,7 +5,7 @@ use milagro_bls::{ }; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; -use serde_hex::{encode as hex_encode, HexVisitor}; +use serde_hex::{encode as hex_encode, PrefixedHexVisitor}; use ssz::{Decode, DecodeError, Encode}; /// A BLS aggregate signature. @@ -173,7 +173,7 @@ impl<'de> Deserialize<'de> for AggregateSignature { where D: Deserializer<'de>, { - let bytes = deserializer.deserialize_str(HexVisitor)?; + let bytes = deserializer.deserialize_str(PrefixedHexVisitor)?; let agg_sig = AggregateSignature::from_ssz_bytes(&bytes) .map_err(|e| serde::de::Error::custom(format!("invalid ssz ({:?})", e)))?; diff --git a/eth2/utils/bls/src/fake_public_key.rs b/eth2/utils/bls/src/fake_public_key.rs index 489c003dd..7395612e7 100644 --- a/eth2/utils/bls/src/fake_public_key.rs +++ b/eth2/utils/bls/src/fake_public_key.rs @@ -3,7 +3,7 @@ use milagro_bls::G1Point; use milagro_bls::PublicKey as RawPublicKey; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; -use serde_hex::{encode as hex_encode, HexVisitor}; +use serde_hex::{encode as hex_encode, PrefixedHexVisitor}; use ssz::{ssz_encode, Decode, DecodeError, Encode}; use std::default; use std::fmt; @@ -124,7 +124,7 @@ impl<'de> Deserialize<'de> for FakePublicKey { where D: Deserializer<'de>, { - let bytes = deserializer.deserialize_str(HexVisitor)?; + let bytes = deserializer.deserialize_str(PrefixedHexVisitor)?; let pubkey = Self::from_ssz_bytes(&bytes[..]) .map_err(|e| serde::de::Error::custom(format!("invalid pubkey ({:?})", e)))?; Ok(pubkey) diff --git a/eth2/utils/bls/src/fake_signature.rs b/eth2/utils/bls/src/fake_signature.rs index 3ece5e87b..9570196d0 100644 --- a/eth2/utils/bls/src/fake_signature.rs +++ b/eth2/utils/bls/src/fake_signature.rs @@ -3,7 +3,7 @@ use hex::encode as hex_encode; use milagro_bls::G2Point; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; -use serde_hex::HexVisitor; +use serde_hex::PrefixedHexVisitor; use ssz::{ssz_encode, Decode, DecodeError, Encode}; /// A single BLS signature. @@ -107,7 +107,7 @@ impl<'de> Deserialize<'de> for FakeSignature { where D: Deserializer<'de>, { - let bytes = deserializer.deserialize_str(HexVisitor)?; + let bytes = deserializer.deserialize_str(PrefixedHexVisitor)?; let pubkey = <_>::from_ssz_bytes(&bytes[..]) .map_err(|e| serde::de::Error::custom(format!("invalid ssz ({:?})", e)))?; Ok(pubkey) diff --git a/eth2/utils/bls/src/macros.rs b/eth2/utils/bls/src/macros.rs index 6809a3c95..6059f0806 100644 --- a/eth2/utils/bls/src/macros.rs +++ b/eth2/utils/bls/src/macros.rs @@ -161,7 +161,7 @@ macro_rules! bytes_struct { where S: serde::ser::Serializer, { - serializer.serialize_str(&hex::encode(ssz::ssz_encode(self))) + serializer.serialize_str(&serde_hex::encode(ssz::ssz_encode(self))) } } @@ -171,7 +171,7 @@ macro_rules! bytes_struct { where D: serde::de::Deserializer<'de>, { - let bytes = deserializer.deserialize_str(serde_hex::HexVisitor)?; + let bytes = deserializer.deserialize_str(serde_hex::PrefixedHexVisitor)?; let signature = Self::from_ssz_bytes(&bytes[..]) .map_err(|e| serde::de::Error::custom(format!("invalid ssz ({:?})", e)))?; Ok(signature) diff --git a/eth2/utils/bls/src/public_key.rs b/eth2/utils/bls/src/public_key.rs index 861e4d99e..789e4faf6 100644 --- a/eth2/utils/bls/src/public_key.rs +++ b/eth2/utils/bls/src/public_key.rs @@ -2,7 +2,7 @@ use super::{SecretKey, BLS_PUBLIC_KEY_BYTE_SIZE}; use milagro_bls::PublicKey as RawPublicKey; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; -use serde_hex::{encode as hex_encode, HexVisitor}; +use serde_hex::{encode as hex_encode, PrefixedHexVisitor}; use ssz::{Decode, DecodeError, Encode}; use std::default; use std::fmt; @@ -110,7 +110,7 @@ impl<'de> Deserialize<'de> for PublicKey { where D: Deserializer<'de>, { - let bytes = deserializer.deserialize_str(HexVisitor)?; + let bytes = deserializer.deserialize_str(PrefixedHexVisitor)?; let pubkey = Self::from_ssz_bytes(&bytes[..]) .map_err(|e| serde::de::Error::custom(format!("invalid pubkey ({:?})", e)))?; Ok(pubkey) diff --git a/eth2/utils/bls/src/secret_key.rs b/eth2/utils/bls/src/secret_key.rs index 6e39cace3..65f02edc9 100644 --- a/eth2/utils/bls/src/secret_key.rs +++ b/eth2/utils/bls/src/secret_key.rs @@ -5,7 +5,7 @@ use hex::encode as hex_encode; use milagro_bls::SecretKey as RawSecretKey; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; -use serde_hex::HexVisitor; +use serde_hex::PrefixedHexVisitor; use ssz::{ssz_encode, Decode, DecodeError, Encode}; /// A single BLS signature. @@ -65,7 +65,7 @@ impl<'de> Deserialize<'de> for SecretKey { where D: Deserializer<'de>, { - let bytes = deserializer.deserialize_str(HexVisitor)?; + let bytes = deserializer.deserialize_str(PrefixedHexVisitor)?; let secret_key = SecretKey::from_ssz_bytes(&bytes[..]) .map_err(|e| serde::de::Error::custom(format!("invalid ssz ({:?})", e)))?; Ok(secret_key) diff --git a/eth2/utils/bls/src/signature.rs b/eth2/utils/bls/src/signature.rs index 64f306b30..b3912f735 100644 --- a/eth2/utils/bls/src/signature.rs +++ b/eth2/utils/bls/src/signature.rs @@ -2,7 +2,7 @@ use super::{PublicKey, SecretKey, BLS_SIG_BYTE_SIZE}; use milagro_bls::Signature as RawSignature; use serde::de::{Deserialize, Deserializer}; use serde::ser::{Serialize, Serializer}; -use serde_hex::{encode as hex_encode, HexVisitor}; +use serde_hex::{encode as hex_encode, PrefixedHexVisitor}; use ssz::{ssz_encode, Decode, DecodeError, Encode}; /// A single BLS signature. @@ -126,7 +126,7 @@ impl<'de> Deserialize<'de> for Signature { where D: Deserializer<'de>, { - let bytes = deserializer.deserialize_str(HexVisitor)?; + let bytes = deserializer.deserialize_str(PrefixedHexVisitor)?; let signature = Self::from_ssz_bytes(&bytes[..]) .map_err(|e| serde::de::Error::custom(format!("invalid ssz ({:?})", e)))?; Ok(signature) diff --git a/eth2/utils/remote_beacon_node/src/lib.rs b/eth2/utils/remote_beacon_node/src/lib.rs index a775ba3a3..55d6d9cbf 100644 --- a/eth2/utils/remote_beacon_node/src/lib.rs +++ b/eth2/utils/remote_beacon_node/src/lib.rs @@ -19,7 +19,10 @@ use types::{ }; use url::Url; -pub use rest_api::{BulkValidatorDutiesRequest, HeadResponse, ValidatorDuty}; +pub use rest_api::{ + CanonicalHeadResponse, Committee, HeadBeaconBlock, ValidatorDutiesRequest, ValidatorDuty, + ValidatorRequest, ValidatorResponse, +}; // Setting a long timeout for debug ensures that crypto-heavy operations can still succeed. #[cfg(debug_assertions)] @@ -224,41 +227,14 @@ impl Validator { } /// Returns the duties required of the given validator pubkeys in the given epoch. - /// - /// ## Warning - /// - /// This method cannot request large amounts of validator duties because the query string fills - /// up the URL. I have seen requests of 1,024 fail. For large requests, use `get_duties_bulk`. pub fn get_duties( &self, epoch: Epoch, validator_pubkeys: &[PublicKey], - ) -> impl Future, Error = Error> { - let validator_pubkeys: Vec = - validator_pubkeys.iter().map(pubkey_as_string).collect(); - - let client = self.0.clone(); - self.url("duties").into_future().and_then(move |url| { - let mut query_params = validator_pubkeys - .into_iter() - .map(|pubkey| ("validator_pubkeys".to_string(), pubkey)) - .collect::>(); - - query_params.push(("epoch".into(), format!("{}", epoch.as_u64()))); - - client.json_get::<_>(url, query_params) - }) - } - - /// Returns the duties required of the given validator pubkeys in the given epoch. - pub fn get_duties_bulk( - &self, - epoch: Epoch, - validator_pubkeys: &[PublicKey], ) -> impl Future, Error = Error> { let client = self.0.clone(); - let bulk_request = BulkValidatorDutiesRequest { + let bulk_request = ValidatorDutiesRequest { epoch, pubkeys: validator_pubkeys .iter() @@ -329,6 +305,7 @@ impl Beacon { .map_err(Into::into) } + /// Returns the genesis time. pub fn get_genesis_time(&self) -> impl Future { let client = self.0.clone(); self.url("genesis_time") @@ -336,6 +313,7 @@ impl Beacon { .and_then(move |url| client.json_get(url, vec![])) } + /// Returns the fork at the head of the beacon chain. pub fn get_fork(&self) -> impl Future { let client = self.0.clone(); self.url("fork") @@ -343,11 +321,20 @@ impl Beacon { .and_then(move |url| client.json_get(url, vec![])) } - pub fn get_head(&self) -> impl Future { + /// Returns info about the head of the canonical beacon chain. + pub fn get_head(&self) -> impl Future { let client = self.0.clone(); self.url("head") .into_future() - .and_then(move |url| client.json_get::(url, vec![])) + .and_then(move |url| client.json_get::(url, vec![])) + } + + /// Returns the set of known beacon chain head blocks. One of these will be the canonical head. + pub fn get_heads(&self) -> impl Future, Error = Error> { + let client = self.0.clone(); + self.url("heads") + .into_future() + .and_then(move |url| client.json_get(url, vec![])) } /// Returns the block and block root at the given slot. @@ -397,6 +384,22 @@ impl Beacon { self.get_state("root".to_string(), root_as_string(root)) } + /// Returns the root of the state at the given slot. + pub fn get_state_root(&self, slot: Slot) -> impl Future { + let client = self.0.clone(); + self.url("state_root").into_future().and_then(move |url| { + client.json_get(url, vec![("slot".into(), format!("{}", slot.as_u64()))]) + }) + } + + /// Returns the root of the block at the given slot. + pub fn get_block_root(&self, slot: Slot) -> impl Future { + let client = self.0.clone(); + self.url("block_root").into_future().and_then(move |url| { + client.json_get(url, vec![("slot".into(), format!("{}", slot.as_u64()))]) + }) + } + /// Returns the state and state root at the given slot. fn get_state( &self, @@ -411,6 +414,86 @@ impl Beacon { }) .map(|response| (response.beacon_state, response.root)) } + + /// Returns the block and block root at the given slot. + /// + /// If `state_root` is `Some`, the query will use the given state instead of the default + /// canonical head state. + pub fn get_validators( + &self, + validator_pubkeys: Vec, + state_root: Option, + ) -> impl Future, Error = Error> { + let client = self.0.clone(); + + let bulk_request = ValidatorRequest { + state_root, + pubkeys: validator_pubkeys + .iter() + .map(|pubkey| pubkey.clone().into()) + .collect(), + }; + + self.url("validators") + .into_future() + .and_then(move |url| client.json_post::<_>(url, bulk_request)) + .and_then(|response| error_for_status(response).map_err(Error::from)) + .and_then(|mut success| success.json().map_err(Error::from)) + } + + /// Returns all validators. + /// + /// If `state_root` is `Some`, the query will use the given state instead of the default + /// canonical head state. + pub fn get_all_validators( + &self, + state_root: Option, + ) -> impl Future, Error = Error> { + let client = self.0.clone(); + + let query_params = if let Some(state_root) = state_root { + vec![("state_root".into(), root_as_string(state_root))] + } else { + vec![] + }; + + self.url("validators/all") + .into_future() + .and_then(move |url| client.json_get(url, query_params)) + } + + /// Returns the active validators. + /// + /// If `state_root` is `Some`, the query will use the given state instead of the default + /// canonical head state. + pub fn get_active_validators( + &self, + state_root: Option, + ) -> impl Future, Error = Error> { + let client = self.0.clone(); + + let query_params = if let Some(state_root) = state_root { + vec![("state_root".into(), root_as_string(state_root))] + } else { + vec![] + }; + + self.url("validators/active") + .into_future() + .and_then(move |url| client.json_get(url, query_params)) + } + + /// Returns committees at the given epoch. + pub fn get_committees( + &self, + epoch: Epoch, + ) -> impl Future, Error = Error> { + let client = self.0.clone(); + + self.url("committees").into_future().and_then(move |url| { + client.json_get(url, vec![("epoch".into(), format!("{}", epoch.as_u64()))]) + }) + } } /// Provides the functions on the `/spec` endpoint of the node. @@ -475,10 +558,6 @@ fn signature_as_string(signature: &Signature) -> String { format!("0x{}", hex::encode(signature.as_ssz_bytes())) } -fn pubkey_as_string(pubkey: &PublicKey) -> String { - format!("0x{}", hex::encode(pubkey.as_ssz_bytes())) -} - impl From for Error { fn from(e: reqwest::Error) -> Error { Error::ReqwestError(e) diff --git a/validator_client/src/duties_service.rs b/validator_client/src/duties_service.rs index 60d3e53d3..ea1f1841a 100644 --- a/validator_client/src/duties_service.rs +++ b/validator_client/src/duties_service.rs @@ -450,7 +450,7 @@ impl DutiesService { .beacon_node .http .validator() - .get_duties_bulk(epoch, pubkeys.as_slice()) + .get_duties(epoch, pubkeys.as_slice()) .map(move |all_duties| (epoch, all_duties)) .map_err(move |e| format!("Failed to get duties for epoch {}: {:?}", epoch, e)) .and_then(move |(epoch, all_duties)| {