use crate::metrics; use beacon_chain::{BeaconChain, BeaconChainError, BeaconChainTypes}; use eth2::types::ProposerData; use fork_choice::ProtoBlock; use slot_clock::SlotClock; use state_processing::per_slot_processing; use types::{BeaconState, Epoch, EthSpec, Hash256, PublicKeyBytes}; /// This sets a maximum bound on the number of epochs to skip whilst instantiating the cache for /// the first time. const EPOCHS_TO_SKIP: u64 = 2; /// Caches the beacon block proposers for a given `epoch` and `epoch_boundary_root`. /// /// This cache is only able to contain a single set of proposers and is only /// intended to cache the proposers for the current epoch according to the head /// of the chain. A change in epoch or re-org to a different chain may cause a /// cache miss and rebuild. pub struct BeaconProposerCache { epoch: Epoch, decision_block_root: Hash256, proposers: Vec, } impl BeaconProposerCache { /// Create a new cache for the current epoch of the `chain`. pub fn new(chain: &BeaconChain) -> Result { let head_root = chain.head_beacon_block_root()?; let head_block = chain .fork_choice .read() .get_block(&head_root) .ok_or_else(|| BeaconChainError::MissingBeaconBlock(head_root))?; // If the head epoch is more than `EPOCHS_TO_SKIP` in the future, just build the cache at // the epoch of the head. This prevents doing a massive amount of skip slots when starting // a new database from genesis. let epoch = { let epoch_now = chain .epoch() .unwrap_or_else(|_| chain.spec.genesis_slot.epoch(T::EthSpec::slots_per_epoch())); let head_epoch = head_block.slot.epoch(T::EthSpec::slots_per_epoch()); if epoch_now > head_epoch + EPOCHS_TO_SKIP { head_epoch } else { epoch_now } }; Self::for_head_block(chain, epoch, head_root, head_block) } /// Create a new cache that contains the shuffling for `current_epoch`, /// assuming that `head_root` and `head_block` represents the most recent /// canonical block. fn for_head_block( chain: &BeaconChain, current_epoch: Epoch, head_root: Hash256, head_block: ProtoBlock, ) -> Result { let _timer = metrics::start_timer(&metrics::HTTP_API_BEACON_PROPOSER_CACHE_TIMES); let mut head_state = chain .get_state(&head_block.state_root, Some(head_block.slot))? .ok_or_else(|| BeaconChainError::MissingBeaconState(head_block.state_root))?; let decision_block_root = Self::decision_block_root(current_epoch, head_root, &head_state)?; // We *must* skip forward to the current epoch to obtain valid proposer // duties. We cannot skip to the previous epoch, like we do with // attester duties. while head_state.current_epoch() < current_epoch { // Skip slots until the current epoch, providing `Hash256::zero()` as the state root // since we don't require it to be valid to identify producers. per_slot_processing(&mut head_state, Some(Hash256::zero()), &chain.spec)?; } let proposers = current_epoch .slot_iter(T::EthSpec::slots_per_epoch()) .map(|slot| { head_state .get_beacon_proposer_index(slot, &chain.spec) .map_err(BeaconChainError::from) .and_then(|i| { let pubkey = chain .validator_pubkey(i)? .ok_or_else(|| BeaconChainError::ValidatorPubkeyCacheIncomplete(i))?; Ok(ProposerData { pubkey: PublicKeyBytes::from(pubkey), validator_index: i as u64, slot, }) }) }) .collect::>()?; Ok(Self { epoch: current_epoch, decision_block_root, proposers, }) } /// Returns a block root which can be used to key the shuffling obtained from the following /// parameters: /// /// - `shuffling_epoch`: the epoch for which the shuffling pertains. /// - `head_block_root`: the block root at the head of the chain. /// - `head_block_state`: the state of `head_block_root`. pub fn decision_block_root( shuffling_epoch: Epoch, head_block_root: Hash256, head_block_state: &BeaconState, ) -> Result { let decision_slot = shuffling_epoch .start_slot(E::slots_per_epoch()) .saturating_sub(1_u64); // If decision slot is equal to or ahead of the head, the block root is the head block root if decision_slot >= head_block_state.slot { Ok(head_block_root) } else { head_block_state .get_block_root(decision_slot) .map(|root| *root) .map_err(Into::into) } } /// Return the proposers for the given `Epoch`. /// /// The cache may be rebuilt if: /// /// - The epoch has changed since the last cache build. /// - There has been a re-org that crosses an epoch boundary. pub fn get_proposers( &mut self, chain: &BeaconChain, epoch: Epoch, ) -> Result, warp::Rejection> { let current_epoch = chain .slot_clock .now_or_genesis() .ok_or_else(|| { warp_utils::reject::custom_server_error("unable to read slot clock".to_string()) })? .epoch(T::EthSpec::slots_per_epoch()); // Disallow requests that are outside the current epoch. This ensures the cache doesn't get // washed-out with old values. if current_epoch != epoch { return Err(warp_utils::reject::custom_bad_request(format!( "requested epoch is {} but only current epoch {} is allowed", epoch, current_epoch ))); } let (head_block_root, head_decision_block_root) = chain .with_head(|head| { Self::decision_block_root(current_epoch, head.beacon_block_root, &head.beacon_state) .map(|decision_root| (head.beacon_block_root, decision_root)) }) .map_err(warp_utils::reject::beacon_chain_error)?; let head_block = chain .fork_choice .read() .get_block(&head_block_root) .ok_or_else(|| BeaconChainError::MissingBeaconBlock(head_block_root)) .map_err(warp_utils::reject::beacon_chain_error)?; // Rebuild the cache if this call causes a cache-miss. if self.epoch != current_epoch || self.decision_block_root != head_decision_block_root { metrics::inc_counter(&metrics::HTTP_API_BEACON_PROPOSER_CACHE_MISSES_TOTAL); *self = Self::for_head_block(chain, current_epoch, head_block_root, head_block) .map_err(warp_utils::reject::beacon_chain_error)?; } else { metrics::inc_counter(&metrics::HTTP_API_BEACON_PROPOSER_CACHE_HITS_TOTAL); } Ok(self.proposers.clone()) } }