Validator API (#504)
* Implemented more REST API endpoints. - Added many of the endpoints, which return 501 - Not Implemented - Created helper function to return a not implemented error - Created a parse_pubkey function to get a PublicKey from a hex string - Created a HTTP handler for the validator endpoints - Started implementing validator/duties endpoint. * Fleshed out get validator duties. - Re-implemented the get validator duties function for the REST API - Added an 'as_hex_string' function to FakePublicKey, beacuse it was missing. * Fixed small caching/state bug. * Extended to array of API inputs. - Created function for getting arrays from GET parameters. - Extended get validator duties function to support array of validator duties. * Tidy API to be more consistent with recent decisions * Addressing Paul's comments. - Cleaning up function to get list of proposers. - Removing unnecessary serde annotations - Clarifying error messages - Only accept pubkeys if they are '0x' prefixed. * Fixed formatting with rustfmt.
This commit is contained in:
		
							parent
							
								
									ce43a59fff
								
							
						
					
					
						commit
						328f11d564
					
				| @ -6,6 +6,7 @@ edition = "2018" | ||||
| 
 | ||||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||
| [dependencies] | ||||
| bls = { path = "../../eth2/utils/bls" } | ||||
| beacon_chain = { path = "../beacon_chain" } | ||||
| network = { path = "../network" } | ||||
| eth2-libp2p = { path = "../eth2-libp2p" } | ||||
| @ -29,3 +30,4 @@ url = "2.0" | ||||
| lazy_static = "1.3.0" | ||||
| lighthouse_metrics = { path = "../../eth2/utils/lighthouse_metrics" } | ||||
| slot_clock = { path = "../../eth2/utils/slot_clock" } | ||||
| hex = "0.3.2" | ||||
|  | ||||
| @ -1,5 +1,10 @@ | ||||
| use crate::ApiError; | ||||
| use crate::{ApiError, ApiResult}; | ||||
| use beacon_chain::{BeaconChain, BeaconChainTypes}; | ||||
| use bls::PublicKey; | ||||
| use hex; | ||||
| use hyper::{Body, Request, StatusCode}; | ||||
| use serde::de::value::StringDeserializer; | ||||
| use serde_json::Deserializer; | ||||
| use store::{iter::AncestorIter, Store}; | ||||
| use types::{BeaconState, EthSpec, Hash256, RelativeEpoch, Slot}; | ||||
| 
 | ||||
| @ -31,6 +36,23 @@ pub fn parse_root(string: &str) -> Result<Hash256, ApiError> { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Parse a PublicKey from a `0x` prefixed hex string
 | ||||
| pub fn parse_pubkey(string: &str) -> Result<PublicKey, ApiError> { | ||||
|     const PREFIX: &str = "0x"; | ||||
|     if string.starts_with(PREFIX) { | ||||
|         let pubkey_bytes = hex::decode(string.trim_start_matches(PREFIX)) | ||||
|             .map_err(|e| ApiError::InvalidQueryParams(format!("Invalid hex string: {:?}", e)))?; | ||||
|         let pubkey = PublicKey::from_bytes(pubkey_bytes.as_slice()).map_err(|e| { | ||||
|             ApiError::InvalidQueryParams(format!("Unable to deserialize public key: {:?}.", e)) | ||||
|         })?; | ||||
|         return Ok(pubkey); | ||||
|     } else { | ||||
|         return Err(ApiError::InvalidQueryParams( | ||||
|             "Public key must have a  '0x' prefix".to_string(), | ||||
|         )); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// Returns the root of the `BeaconBlock` in the canonical chain of `beacon_chain` at the given
 | ||||
| /// `slot`, if possible.
 | ||||
| ///
 | ||||
| @ -143,6 +165,12 @@ pub fn state_root_at_slot<T: BeaconChainTypes>( | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| pub fn implementation_pending_response(_req: Request<Body>) -> ApiResult { | ||||
|     Err(ApiError::NotImplemented( | ||||
|         "API endpoint has not yet been implemented, but is planned to be soon.".to_owned(), | ||||
|     )) | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod test { | ||||
|     use super::*; | ||||
|  | ||||
| @ -10,6 +10,7 @@ mod network; | ||||
| mod node; | ||||
| mod spec; | ||||
| mod url_query; | ||||
| mod validator; | ||||
| 
 | ||||
| use beacon_chain::{BeaconChain, BeaconChainTypes}; | ||||
| use client_network::Service as NetworkService; | ||||
| @ -123,14 +124,26 @@ pub fn start_server<T: BeaconChainTypes>( | ||||
| 
 | ||||
|             // Route the request to the correct handler.
 | ||||
|             let result = match (req.method(), path.as_ref()) { | ||||
|                 // Methods for Beacon Node
 | ||||
|                 //TODO: Remove?
 | ||||
|                 //(&Method::GET, "/beacon/best_slot") => beacon::get_best_slot::<T>(req),
 | ||||
|                 (&Method::GET, "/beacon/head") => beacon::get_head::<T>(req), | ||||
|                 (&Method::GET, "/beacon/block") => beacon::get_block::<T>(req), | ||||
|                 (&Method::GET, "/beacon/blocks") => helpers::implementation_pending_response(req), | ||||
|                 //TODO Is the below replaced by finalized_checkpoint?
 | ||||
|                 (&Method::GET, "/beacon/chainhead") => { | ||||
|                     helpers::implementation_pending_response(req) | ||||
|                 } | ||||
|                 (&Method::GET, "/beacon/block_root") => beacon::get_block_root::<T>(req), | ||||
|                 (&Method::GET, "/beacon/latest_finalized_checkpoint") => { | ||||
|                     beacon::get_latest_finalized_checkpoint::<T>(req) | ||||
|                 } | ||||
|                 (&Method::GET, "/beacon/state") => beacon::get_state::<T>(req), | ||||
|                 (&Method::GET, "/beacon/state_root") => beacon::get_state_root::<T>(req), | ||||
| 
 | ||||
|                 //TODO: Add aggreggate/filtered state lookups here, e.g. /beacon/validators/balances
 | ||||
| 
 | ||||
|                 // Methods for Client
 | ||||
|                 (&Method::GET, "/metrics") => metrics::get_prometheus::<T>(req), | ||||
|                 (&Method::GET, "/network/enr") => network::get_enr::<T>(req), | ||||
|                 (&Method::GET, "/network/peer_count") => network::get_peer_count::<T>(req), | ||||
| @ -142,9 +155,40 @@ pub fn start_server<T: BeaconChainTypes>( | ||||
|                 } | ||||
|                 (&Method::GET, "/node/version") => node::get_version(req), | ||||
|                 (&Method::GET, "/node/genesis_time") => node::get_genesis_time::<T>(req), | ||||
|                 (&Method::GET, "/node/deposit_contract") => { | ||||
|                     helpers::implementation_pending_response(req) | ||||
|                 } | ||||
|                 (&Method::GET, "/node/syncing") => helpers::implementation_pending_response(req), | ||||
|                 (&Method::GET, "/node/fork") => helpers::implementation_pending_response(req), | ||||
| 
 | ||||
|                 // Methods for Network
 | ||||
|                 (&Method::GET, "/network/enr") => network::get_enr::<T>(req), | ||||
|                 (&Method::GET, "/network/peer_count") => network::get_peer_count::<T>(req), | ||||
|                 (&Method::GET, "/network/peer_id") => network::get_peer_id::<T>(req), | ||||
|                 (&Method::GET, "/network/peers") => network::get_peer_list::<T>(req), | ||||
|                 (&Method::GET, "/network/listen_addresses") => { | ||||
|                     network::get_listen_addresses::<T>(req) | ||||
|                 } | ||||
| 
 | ||||
|                 // Methods for Validator
 | ||||
|                 (&Method::GET, "/validator/duties") => validator::get_validator_duties::<T>(req), | ||||
|                 (&Method::GET, "/validator/block") => helpers::implementation_pending_response(req), | ||||
|                 (&Method::POST, "/validator/block") => { | ||||
|                     helpers::implementation_pending_response(req) | ||||
|                 } | ||||
|                 (&Method::GET, "/validator/attestation") => { | ||||
|                     helpers::implementation_pending_response(req) | ||||
|                 } | ||||
|                 (&Method::POST, "/validator/attestation") => { | ||||
|                     helpers::implementation_pending_response(req) | ||||
|                 } | ||||
| 
 | ||||
|                 (&Method::GET, "/spec") => spec::get_spec::<T>(req), | ||||
|                 (&Method::GET, "/spec/slots_per_epoch") => spec::get_slots_per_epoch::<T>(req), | ||||
|                 _ => Err(ApiError::MethodNotAllowed(path.clone())), | ||||
| 
 | ||||
|                 _ => Err(ApiError::NotFound( | ||||
|                     "Request path and/or method not found.".to_owned(), | ||||
|                 )), | ||||
|             }; | ||||
| 
 | ||||
|             let response = match result { | ||||
|  | ||||
| @ -2,6 +2,8 @@ use crate::ApiError; | ||||
| use hyper::Request; | ||||
| 
 | ||||
| /// Provides handy functions for parsing the query parameters of a URL.
 | ||||
| 
 | ||||
| #[derive(Clone, Copy)] | ||||
| pub struct UrlQuery<'a>(url::form_urlencoded::Parse<'a>); | ||||
| 
 | ||||
| impl<'a> UrlQuery<'a> { | ||||
| @ -11,9 +13,7 @@ impl<'a> UrlQuery<'a> { | ||||
|     pub fn from_request<T>(req: &'a Request<T>) -> Result<Self, ApiError> { | ||||
|         let query_str = req.uri().query().ok_or_else(|| { | ||||
|             ApiError::InvalidQueryParams( | ||||
|                 "URL query must be valid and contain at least one
 | ||||
|                                          key." | ||||
|                     .to_string(), | ||||
|                 "URL query must be valid and contain at least one key.".to_string(), | ||||
|             ) | ||||
|         })?; | ||||
| 
 | ||||
| @ -60,6 +60,23 @@ impl<'a> UrlQuery<'a> { | ||||
|             ))) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Returns a vector of all values present where `key` is in `keys
 | ||||
|     ///
 | ||||
|     /// If no match is found, an `InvalidQueryParams` error is returned.
 | ||||
|     pub fn all_of(mut self, key: &str) -> Result<Vec<String>, ApiError> { | ||||
|         let queries: Vec<_> = self | ||||
|             .0 | ||||
|             .filter_map(|(k, v)| { | ||||
|                 if k.eq(key) { | ||||
|                     Some(v.into_owned()) | ||||
|                 } else { | ||||
|                     None | ||||
|                 } | ||||
|             }) | ||||
|             .collect(); | ||||
|         Ok(queries) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
|  | ||||
							
								
								
									
										149
									
								
								beacon_node/rest_api/src/validator.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								beacon_node/rest_api/src/validator.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,149 @@ | ||||
| use super::{success_response, ApiResult}; | ||||
| use crate::{helpers::*, ApiError, UrlQuery}; | ||||
| use beacon_chain::{BeaconChain, BeaconChainTypes}; | ||||
| use bls::PublicKey; | ||||
| use hyper::{Body, Request}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::sync::Arc; | ||||
| use store::Store; | ||||
| use types::beacon_state::EthSpec; | ||||
| use types::{BeaconBlock, BeaconState, Epoch, RelativeEpoch, Shard, Slot}; | ||||
| 
 | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| pub struct ValidatorDuty { | ||||
|     /// The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._
 | ||||
|     pub validator_pubkey: String, | ||||
|     /// The slot at which the validator must attest.
 | ||||
|     pub attestation_slot: Option<Slot>, | ||||
|     /// The shard in which the validator must attest.
 | ||||
|     pub attestation_shard: Option<Shard>, | ||||
|     /// The slot in which a validator must propose a block, or `null` if block production is not required.
 | ||||
|     pub block_proposal_slot: Option<Slot>, | ||||
| } | ||||
| 
 | ||||
| impl ValidatorDuty { | ||||
|     pub fn new() -> ValidatorDuty { | ||||
|         ValidatorDuty { | ||||
|             validator_pubkey: "".to_string(), | ||||
|             attestation_slot: None, | ||||
|             attestation_shard: None, | ||||
|             block_proposal_slot: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /// HTTP Handler to retrieve a the duties for a set of validators during a particular epoch
 | ||||
| pub fn get_validator_duties<T: BeaconChainTypes + 'static>(req: Request<Body>) -> ApiResult { | ||||
|     // Get beacon state
 | ||||
|     let beacon_chain = req | ||||
|         .extensions() | ||||
|         .get::<Arc<BeaconChain<T>>>() | ||||
|         .ok_or_else(|| ApiError::ServerError("Beacon chain extension missing".to_string()))?; | ||||
|     let _ = beacon_chain | ||||
|         .ensure_state_caches_are_built() | ||||
|         .map_err(|e| ApiError::ServerError(format!("Unable to build state caches: {:?}", e)))?; | ||||
|     let head_state = beacon_chain | ||||
|         .speculative_state() | ||||
|         .expect("This is legacy code and should be removed."); | ||||
| 
 | ||||
|     // Parse and check query parameters
 | ||||
|     let query = UrlQuery::from_request(&req)?; | ||||
| 
 | ||||
|     let current_epoch = head_state.current_epoch(); | ||||
|     let epoch = match query.first_of(&["epoch"]) { | ||||
|         Ok((_, v)) => Epoch::new(v.parse::<u64>().map_err(|e| { | ||||
|             ApiError::InvalidQueryParams(format!("Invalid epoch parameter, must be a u64. {:?}", e)) | ||||
|         })?), | ||||
|         Err(_) => { | ||||
|             // epoch not supplied, use the current epoch
 | ||||
|             current_epoch | ||||
|         } | ||||
|     }; | ||||
|     let relative_epoch = RelativeEpoch::from_epoch(current_epoch, epoch).map_err(|e| { | ||||
|         ApiError::InvalidQueryParams(format!( | ||||
|             "Cannot get RelativeEpoch, epoch out of range: {:?}", | ||||
|             e | ||||
|         )) | ||||
|     })?; | ||||
|     //TODO: Handle an array of validators, currently only takes one
 | ||||
|     let mut validators: Vec<PublicKey> = match query.all_of("validator_pubkeys") { | ||||
|         Ok(v) => v | ||||
|             .iter() | ||||
|             .map(|pk| parse_pubkey(pk)) | ||||
|             .collect::<Result<Vec<_>, _>>()?, | ||||
|         Err(e) => { | ||||
|             return Err(e); | ||||
|         } | ||||
|     }; | ||||
|     let mut duties: Vec<ValidatorDuty> = Vec::new(); | ||||
| 
 | ||||
|     // Get a list of all validators for this epoch
 | ||||
|     let validator_proposers: Vec<usize> = epoch | ||||
|         .slot_iter(T::EthSpec::slots_per_epoch()) | ||||
|         .map(|slot| { | ||||
|             head_state | ||||
|                 .get_beacon_proposer_index(slot, relative_epoch, &beacon_chain.spec) | ||||
|                 .map_err(|e| { | ||||
|                     ApiError::ServerError(format!( | ||||
|                         "Unable to get proposer index for validator: {:?}", | ||||
|                         e | ||||
|                     )) | ||||
|                 }) | ||||
|         }) | ||||
|         .collect::<Result<Vec<usize>, _>>()?; | ||||
| 
 | ||||
|     // Look up duties for each validator
 | ||||
|     for val_pk in validators { | ||||
|         let mut duty = ValidatorDuty::new(); | ||||
|         duty.validator_pubkey = val_pk.as_hex_string(); | ||||
| 
 | ||||
|         // Get the validator index
 | ||||
|         // If it does not exist in the index, just add a null duty and move on.
 | ||||
|         let val_index: usize = match head_state.get_validator_index(&val_pk) { | ||||
|             Ok(Some(i)) => i, | ||||
|             Ok(None) => { | ||||
|                 duties.append(&mut vec![duty]); | ||||
|                 continue; | ||||
|             } | ||||
|             Err(e) => { | ||||
|                 return Err(ApiError::ServerError(format!( | ||||
|                     "Unable to read validator index cache. {:?}", | ||||
|                     e | ||||
|                 ))); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         // Set attestation duties
 | ||||
|         match head_state.get_attestation_duties(val_index, relative_epoch) { | ||||
|             Ok(Some(d)) => { | ||||
|                 duty.attestation_slot = Some(d.slot); | ||||
|                 duty.attestation_shard = Some(d.shard); | ||||
|             } | ||||
|             Ok(None) => {} | ||||
|             Err(e) => { | ||||
|                 return Err(ApiError::ServerError(format!( | ||||
|                     "unable to read cache for attestation duties: {:?}", | ||||
|                     e | ||||
|                 ))) | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         // If the validator is to propose a block, identify the slot
 | ||||
|         if let Some(slot) = validator_proposers.iter().position(|&v| val_index == v) { | ||||
|             duty.block_proposal_slot = Some(Slot::new( | ||||
|                 relative_epoch | ||||
|                     .into_epoch(current_epoch) | ||||
|                     .start_slot(T::EthSpec::slots_per_epoch()) | ||||
|                     .as_u64() | ||||
|                     + slot as u64, | ||||
|             )); | ||||
|         } | ||||
| 
 | ||||
|         duties.append(&mut vec![duty]); | ||||
|     } | ||||
|     let body = Body::from( | ||||
|         serde_json::to_string(&duties) | ||||
|             .expect("We should always be able to serialize the duties we created."), | ||||
|     ); | ||||
|     Ok(success_response(body)) | ||||
| } | ||||
| @ -61,6 +61,13 @@ impl FakePublicKey { | ||||
|         hex_encode(end_bytes) | ||||
|     } | ||||
| 
 | ||||
|     /// Returns the point as a hex string of the SSZ encoding.
 | ||||
|     ///
 | ||||
|     /// Note: the string is prefixed with `0x`.
 | ||||
|     pub fn as_hex_string(&self) -> String { | ||||
|         hex_encode(self.as_ssz_bytes()) | ||||
|     } | ||||
| 
 | ||||
|     // Returns itself
 | ||||
|     pub fn as_raw(&self) -> &Self { | ||||
|         self | ||||
|  | ||||
| @ -28,7 +28,7 @@ impl PublicKey { | ||||
|     /// Returns the underlying point as compressed bytes.
 | ||||
|     ///
 | ||||
|     /// Identical to `self.as_uncompressed_bytes()`.
 | ||||
|     fn as_bytes(&self) -> Vec<u8> { | ||||
|     pub fn as_bytes(&self) -> Vec<u8> { | ||||
|         self.as_raw().as_bytes() | ||||
|     } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user