diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index ff4d46efcb..606dfb64dc 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -657,26 +657,41 @@ pub fn serve( .and(warp::path::end()) .and_then( |state_id: StateId, chain: Arc>, query: api_types::CommitteesQuery| { - // the api spec says if the epoch is not present then the epoch of the state should be used - let query_state_id = query.epoch.map_or(state_id, |epoch| { - StateId::slot(epoch.start_slot(T::EthSpec::slots_per_epoch())) - }); - blocking_json_task(move || { - query_state_id.map_state(&chain, |state| { - let epoch = state.slot().epoch(T::EthSpec::slots_per_epoch()); + state_id.map_state(&chain, |state| { + let current_epoch = state.current_epoch(); + let epoch = query.epoch.unwrap_or(current_epoch); - let committee_cache = if state - .committee_cache_is_initialized(RelativeEpoch::Current) + let committee_cache = match RelativeEpoch::from_epoch(current_epoch, epoch) { - state - .committee_cache(RelativeEpoch::Current) - .map(Cow::Borrowed) - } else { - CommitteeCache::initialized(state, epoch, &chain.spec).map(Cow::Owned) + Ok(relative_epoch) + if state.committee_cache_is_initialized(relative_epoch) => + { + state.committee_cache(relative_epoch).map(Cow::Borrowed) + } + _ => CommitteeCache::initialized(state, epoch, &chain.spec) + .map(Cow::Owned), } - .map_err(BeaconChainError::BeaconStateError) - .map_err(warp_utils::reject::beacon_chain_error)?; + .map_err(|e| match e { + BeaconStateError::EpochOutOfBounds => { + let max_sprp = T::EthSpec::slots_per_historical_root() as u64; + let first_subsequent_restore_point_slot = + ((epoch.start_slot(T::EthSpec::slots_per_epoch()) / max_sprp) + + 1) + * max_sprp; + if epoch < current_epoch { + warp_utils::reject::custom_bad_request(format!( + "epoch out of bounds, try state at slot {}", + first_subsequent_restore_point_slot, + )) + } else { + warp_utils::reject::custom_bad_request( + "epoch out of bounds, too far in future".into(), + ) + } + } + _ => warp_utils::reject::beacon_chain_error(e.into()), + })?; // Use either the supplied slot or all slots in the epoch. let slots = query.slot.map(|slot| vec![slot]).unwrap_or_else(|| { diff --git a/consensus/types/src/beacon_state.rs b/consensus/types/src/beacon_state.rs index 66656d3589..fca200312f 100644 --- a/consensus/types/src/beacon_state.rs +++ b/consensus/types/src/beacon_state.rs @@ -963,6 +963,13 @@ impl BeaconState { } } + /// Return the minimum epoch for which `get_randao_mix` will return a non-error value. + pub fn min_randao_epoch(&self) -> Epoch { + self.current_epoch() + .saturating_add(1u64) + .saturating_sub(T::EpochsPerHistoricalVector::to_u64()) + } + /// XOR-assigns the existing `epoch` randao mix with the hash of the `signature`. /// /// # Errors: diff --git a/consensus/types/src/beacon_state/committee_cache.rs b/consensus/types/src/beacon_state/committee_cache.rs index 8a87cddac8..7a526acc58 100644 --- a/consensus/types/src/beacon_state/committee_cache.rs +++ b/consensus/types/src/beacon_state/committee_cache.rs @@ -38,8 +38,18 @@ impl CommitteeCache { epoch: Epoch, spec: &ChainSpec, ) -> Result { - RelativeEpoch::from_epoch(state.current_epoch(), epoch) - .map_err(|_| Error::EpochOutOfBounds)?; + // Check that the cache is being built for an in-range epoch. + // + // We allow caches to be constructed for historic epochs, per: + // + // https://github.com/sigp/lighthouse/issues/3270 + let reqd_randao_epoch = epoch + .saturating_sub(spec.min_seed_lookahead) + .saturating_sub(1u64); + + if reqd_randao_epoch < state.min_randao_epoch() || epoch > state.current_epoch() + 1 { + return Err(Error::EpochOutOfBounds); + } // May cause divide-by-zero errors. if T::slots_per_epoch() == 0 { diff --git a/consensus/types/src/beacon_state/committee_cache/tests.rs b/consensus/types/src/beacon_state/committee_cache/tests.rs index db431138aa..11cc6095da 100644 --- a/consensus/types/src/beacon_state/committee_cache/tests.rs +++ b/consensus/types/src/beacon_state/committee_cache/tests.rs @@ -42,7 +42,7 @@ async fn new_state(validator_count: usize, slot: Slot) -> BeaconStat .add_attested_blocks_at_slots( head_state, Hash256::zero(), - (1..slot.as_u64()) + (1..=slot.as_u64()) .map(Slot::new) .collect::>() .as_slice(), @@ -86,6 +86,8 @@ async fn shuffles_for_the_right_epoch() { let mut state = new_state::(num_validators, slot).await; let spec = &MinimalEthSpec::default_spec(); + assert_eq!(state.current_epoch(), epoch); + let distinct_hashes: Vec = (0..MinimalEthSpec::epochs_per_historical_vector()) .map(|i| Hash256::from_low_u64_be(i as u64)) .collect(); @@ -124,15 +126,41 @@ async fn shuffles_for_the_right_epoch() { } }; - let cache = CommitteeCache::initialized(&state, state.current_epoch(), spec).unwrap(); - assert_eq!(cache.shuffling(), shuffling_with_seed(current_seed)); - assert_shuffling_positions_accurate(&cache); + // We can initialize the committee cache at recent epochs in the past, and one epoch into the + // future. + for e in (0..=epoch.as_u64() + 1).map(Epoch::new) { + let seed = state.get_seed(e, Domain::BeaconAttester, spec).unwrap(); + let cache = CommitteeCache::initialized(&state, e, spec) + .unwrap_or_else(|_| panic!("failed at epoch {}", e)); + assert_eq!(cache.shuffling(), shuffling_with_seed(seed)); + assert_shuffling_positions_accurate(&cache); + } - let cache = CommitteeCache::initialized(&state, state.previous_epoch(), spec).unwrap(); - assert_eq!(cache.shuffling(), shuffling_with_seed(previous_seed)); - assert_shuffling_positions_accurate(&cache); - - let cache = CommitteeCache::initialized(&state, state.next_epoch().unwrap(), spec).unwrap(); - assert_eq!(cache.shuffling(), shuffling_with_seed(next_seed)); - assert_shuffling_positions_accurate(&cache); + // We should *not* be able to build a committee cache for the epoch after the next epoch. + assert_eq!( + CommitteeCache::initialized(&state, epoch + 2, spec), + Err(BeaconStateError::EpochOutOfBounds) + ); +} + +#[tokio::test] +async fn min_randao_epoch_correct() { + let num_validators = MinimalEthSpec::minimum_validator_count() * 2; + let current_epoch = Epoch::new(MinimalEthSpec::epochs_per_historical_vector() as u64 * 2); + + let mut state = new_state::( + num_validators, + Epoch::new(1).start_slot(MinimalEthSpec::slots_per_epoch()), + ) + .await; + + // Override the epoch so that there's some room to move. + *state.slot_mut() = current_epoch.start_slot(MinimalEthSpec::slots_per_epoch()); + assert_eq!(state.current_epoch(), current_epoch); + + // The min_randao_epoch should be the minimum epoch such that `get_randao_mix` returns `Ok`. + let min_randao_epoch = state.min_randao_epoch(); + state.get_randao_mix(min_randao_epoch).unwrap(); + state.get_randao_mix(min_randao_epoch - 1).unwrap_err(); + state.get_randao_mix(min_randao_epoch + 1).unwrap(); }