diff --git a/beacon_node/rest_api/src/lib.rs b/beacon_node/rest_api/src/lib.rs index a5d434367b..bea1ccbb9f 100644 --- a/beacon_node/rest_api/src/lib.rs +++ b/beacon_node/rest_api/src/lib.rs @@ -40,7 +40,7 @@ use url_query::UrlQuery; pub use crate::helpers::parse_pubkey; pub use beacon::{BlockResponse, HeadResponse, StateResponse}; pub use config::Config; -pub use validator::ValidatorDuty; +pub use validator::{BulkValidatorDutiesRequest, 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 f0dc4e2c5a..35bde8eed9 100644 --- a/beacon_node/rest_api/src/router.rs +++ b/beacon_node/rest_api/src/router.rs @@ -96,7 +96,10 @@ pub fn route( // Methods for Validator (&Method::GET, "/validator/duties") => { - into_boxfut(validator::get_validator_duties::(req, beacon_chain, log)) + into_boxfut(validator::get_validator_duties::(req, beacon_chain)) + } + (&Method::POST, "/validator/duties") => { + validator::post_validator_duties::(req, beacon_chain) } (&Method::GET, "/validator/block") => { into_boxfut(validator::get_new_beacon_block::(req, beacon_chain)) diff --git a/beacon_node/rest_api/src/validator.rs b/beacon_node/rest_api/src/validator.rs index 0d2a982b17..c7029e5a57 100644 --- a/beacon_node/rest_api/src/validator.rs +++ b/beacon_node/rest_api/src/validator.rs @@ -12,10 +12,11 @@ use futures::future::Future; use futures::stream::Stream; use hyper::{Body, Request}; use serde::{Deserialize, Serialize}; -use slog::{info, trace, warn, Logger}; +use slog::{info, warn, Logger}; +use ssz_derive::{Decode, Encode}; use std::sync::Arc; use types::beacon_state::EthSpec; -use types::{Attestation, BeaconBlock, CommitteeIndex, RelativeEpoch, Slot}; +use types::{Attestation, BeaconBlock, CommitteeIndex, Epoch, RelativeEpoch, Slot}; #[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] pub struct ValidatorDuty { @@ -31,20 +32,67 @@ pub struct ValidatorDuty { pub block_proposal_slot: Option, } +#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, Encode, Decode)] +pub struct BulkValidatorDutiesRequest { + pub epoch: Epoch, + pub pubkeys: Vec, +} + +/// HTTP Handler to retrieve a the duties for a set of validators during a particular epoch. This +/// method allows for collecting bulk sets of validator duties without risking exceeding the max +/// URL length with query pairs. +pub fn post_validator_duties( + 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 BulkValidatorDutiesRequest: {:?}", + e + )) + }) + }) + .and_then(|bulk_request| { + return_validator_duties(beacon_chain, bulk_request.epoch, bulk_request.pubkeys) + }) + .and_then(|duties| response_builder?.body_no_ssz(&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( req: Request, beacon_chain: Arc>, - log: Logger, ) -> ApiResult { - slog::trace!(log, "Validator duties requested of API: {:?}", &req); - let query = UrlQuery::from_request(&req)?; let epoch = query.epoch()?; + let validator_pubkeys = query + .all_of("validator_pubkeys")? + .iter() + .map(|validator_pubkey_str| parse_pubkey(validator_pubkey_str)) + .collect::>()?; + let duties = return_validator_duties(beacon_chain, epoch, validator_pubkeys)?; + + ResponseBuilder::new(&req)?.body_no_ssz(&duties) +} + +fn return_validator_duties( + beacon_chain: Arc>, + epoch: Epoch, + validator_pubkeys: Vec, +) -> Result, ApiError> { let mut state = beacon_chain .state_at_slot(epoch.start_slot(T::EthSpec::slots_per_epoch())) .map_err(|e| { @@ -83,51 +131,46 @@ pub fn get_validator_duties( }) .collect::, _>>()?; - let duties = query - .all_of("validator_pubkeys")? - .iter() - .map(|validator_pubkey_str| { - parse_pubkey(validator_pubkey_str).and_then(|validator_pubkey| { - if let Some(validator_index) = - state.get_validator_index(&validator_pubkey).map_err(|e| { - ApiError::ServerError(format!("Unable to read pubkey cache: {:?}", e)) - })? - { - let duties = state - .get_attestation_duties(validator_index, relative_epoch) - .map_err(|e| { - ApiError::ServerError(format!( - "Unable to obtain attestation duties: {:?}", - e - )) - })?; + validator_pubkeys + .into_iter() + .map(|validator_pubkey| { + if let Some(validator_index) = + state.get_validator_index(&validator_pubkey).map_err(|e| { + ApiError::ServerError(format!("Unable to read pubkey cache: {:?}", e)) + })? + { + let duties = state + .get_attestation_duties(validator_index, relative_epoch) + .map_err(|e| { + ApiError::ServerError(format!( + "Unable to obtain attestation duties: {:?}", + e + )) + })?; - let block_proposal_slot = validator_proposers - .iter() - .find(|(i, _slot)| validator_index == *i) - .map(|(_i, slot)| *slot); + let block_proposal_slot = validator_proposers + .iter() + .find(|(i, _slot)| validator_index == *i) + .map(|(_i, slot)| *slot); - Ok(ValidatorDuty { - validator_pubkey, - attestation_slot: duties.map(|d| d.slot), - attestation_committee_index: duties.map(|d| d.index), - attestation_committee_position: duties.map(|d| d.committee_position), - block_proposal_slot, - }) - } else { - Ok(ValidatorDuty { - validator_pubkey, - attestation_slot: None, - attestation_committee_index: None, - attestation_committee_position: None, - block_proposal_slot: None, - }) - } - }) + Ok(ValidatorDuty { + validator_pubkey, + attestation_slot: duties.map(|d| d.slot), + attestation_committee_index: duties.map(|d| d.index), + attestation_committee_position: duties.map(|d| d.committee_position), + block_proposal_slot, + }) + } else { + Ok(ValidatorDuty { + validator_pubkey, + attestation_slot: None, + attestation_committee_index: None, + attestation_committee_position: None, + block_proposal_slot: None, + }) + } }) - .collect::, ApiError>>()?; - - ResponseBuilder::new(&req)?.body_no_ssz(&duties) + .collect::, ApiError>>() } /// HTTP Handler to produce a new BeaconBlock from the current state, ready to be signed by a validator. @@ -163,10 +206,6 @@ pub fn publish_beacon_block( let response_builder = ResponseBuilder::new(&req); let body = req.into_body(); - trace!( - log, - "Got the request body, now going to parse it into a block." - ); Box::new(body .concat2() .map_err(|e| ApiError::ServerError(format!("Unable to get request body: {:?}",e))) diff --git a/beacon_node/rest_api/tests/test.rs b/beacon_node/rest_api/tests/test.rs index 38c52fd807..47cb670544 100644 --- a/beacon_node/rest_api/tests/test.rs +++ b/beacon_node/rest_api/tests/test.rs @@ -5,12 +5,12 @@ use node_test_rig::{ environment::{Environment, EnvironmentBuilder}, testing_client_config, ClientGenesis, LocalBeaconNode, }; -use remote_beacon_node::PublishStatus; +use remote_beacon_node::{PublishStatus, ValidatorDuty}; use std::sync::Arc; use tree_hash::{SignedRoot, TreeHash}; use types::{ test_utils::generate_deterministic_keypair, BeaconBlock, ChainSpec, Domain, Epoch, EthSpec, - MinimalEthSpec, RelativeEpoch, Signature, Slot, + MinimalEthSpec, PublicKey, RelativeEpoch, Signature, Slot, }; type E = MinimalEthSpec; @@ -159,6 +159,43 @@ fn validator_produce_attestation() { ); } +#[test] +fn validator_duties_bulk() { + let mut env = build_env(); + + let spec = &E::default_spec(); + + let node = LocalBeaconNode::production(env.core_context(), 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.clone()) + .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(); @@ -188,6 +225,16 @@ fn validator_duties() { .block_on(remote_node.http.validator().get_duties(epoch, &validators)) .expect("should fetch duties from http api"); + check_duties(duties, epoch, validators, beacon_chain, spec); +} + +fn check_duties( + duties: Vec, + epoch: Epoch, + validators: Vec, + beacon_chain: Arc>, + spec: &ChainSpec, +) { assert_eq!( validators.len(), duties.len(), diff --git a/eth2/utils/remote_beacon_node/src/lib.rs b/eth2/utils/remote_beacon_node/src/lib.rs index 369de9159e..180f325adb 100644 --- a/eth2/utils/remote_beacon_node/src/lib.rs +++ b/eth2/utils/remote_beacon_node/src/lib.rs @@ -20,7 +20,7 @@ use types::{ }; use url::Url; -pub use rest_api::{HeadResponse, ValidatorDuty}; +pub use rest_api::{BulkValidatorDutiesRequest, HeadResponse, ValidatorDuty}; pub const REQUEST_TIMEOUT_SECONDS: u64 = 5; @@ -213,6 +213,11 @@ 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, @@ -234,6 +239,26 @@ impl Validator { }) } + /// 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 { + epoch, + pubkeys: validator_pubkeys.to_vec(), + }; + + self.url("duties") + .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)) + } + /// Posts a block to the beacon node, expecting it to verify it and publish it to the network. pub fn publish_block( &self,