Consensus updates for v0.12 (#1228)

* Update state processing for v0.12

* Fix EF test runners for v0.12

* Fix some tests

* Fix broken attestation verification test

* More test fixes

* Fix typo found in review
This commit is contained in:
Michael Sproul
2020-06-03 13:34:01 +10:00
parent 197adeff0b
commit fe03ff0f21
29 changed files with 359 additions and 323 deletions

View File

@@ -157,6 +157,15 @@ pub fn process_block_header<T: EthSpec>(
// Verify that the slots match
verify!(block.slot == state.slot, HeaderInvalid::StateSlotMismatch);
// Verify that the block is newer than the latest block header
verify!(
block.slot > state.latest_block_header.slot,
HeaderInvalid::OlderThanLatestBlockHeader {
block_slot: block.slot,
latest_block_header_slot: state.latest_block_header.slot,
}
);
// Verify that proposer index is the correct index
let proposer_index = block.proposer_index as usize;
let state_proposer_index = state.get_beacon_proposer_index(block.slot, spec)?;

View File

@@ -176,6 +176,10 @@ impl<T> From<ArithError> for BlockOperationError<T> {
pub enum HeaderInvalid {
ProposalSignatureInvalid,
StateSlotMismatch,
OlderThanLatestBlockHeader {
latest_block_header_slot: Slot,
block_slot: Slot,
},
ProposerIndexMismatch {
block_proposer_index: usize,
state_proposer_index: usize,
@@ -255,9 +259,6 @@ pub enum AttestationInvalid {
attestation: Checkpoint,
is_current: bool,
},
/// There are no set bits on the attestation -- an attestation must be signed by at least one
/// validator.
AggregationBitfieldIsEmpty,
/// The aggregation bitfield length is not the smallest possible size to represent the committee.
BadAggregationBitfieldLength {
committee_len: usize,
@@ -291,10 +292,8 @@ impl From<BlockOperationError<IndexedAttestationInvalid>>
#[derive(Debug, PartialEq, Clone)]
pub enum IndexedAttestationInvalid {
/// The number of indices exceeds the global maximum.
///
/// (max_indices, indices_given)
MaxIndicesExceed(usize, usize),
/// The number of indices is 0.
IndicesEmpty,
/// The validator indices were not in increasing order.
///
/// The error occurred between the given `index` and `index + 1`

View File

@@ -20,11 +20,8 @@ pub fn is_valid_indexed_attestation<T: EthSpec>(
) -> Result<()> {
let indices = &indexed_attestation.attesting_indices;
// Verify max number of indices
verify!(
indices.len() <= T::MaxValidatorsPerCommittee::to_usize(),
Invalid::MaxIndicesExceed(T::MaxValidatorsPerCommittee::to_usize(), indices.len())
);
// Verify that indices aren't empty
verify!(!indices.is_empty(), Invalid::IndicesEmpty);
// Check that indices are sorted and unique
let check_sorted = |list: &[u64]| -> Result<()> {

View File

@@ -11,7 +11,7 @@ use types::{
AggregateSignature, AttesterSlashing, BeaconBlock, BeaconState, BeaconStateError, ChainSpec,
DepositData, Domain, EthSpec, Fork, Hash256, IndexedAttestation, ProposerSlashing, PublicKey,
Signature, SignedAggregateAndProof, SignedBeaconBlock, SignedBeaconBlockHeader, SignedRoot,
SignedVoluntaryExit, SigningRoot,
SignedVoluntaryExit, SigningData,
};
pub type Result<T> = std::result::Result<T, Error>;
@@ -92,7 +92,7 @@ where
);
let message = if let Some(root) = block_root {
SigningRoot {
SigningData {
object_root: root,
domain,
}

View File

@@ -58,13 +58,6 @@ pub fn verify_attestation_for_state<T: EthSpec>(
) -> Result<()> {
let data = &attestation.data;
// This emptiness check is required *in addition* to the length check in `get_attesting_indices`
// because we can parse a bitfield and know its length, even if it has no bits set.
verify!(
!attestation.aggregation_bits.is_zero(),
Invalid::AggregationBitfieldIsEmpty
);
verify!(
data.index < state.get_committee_count_at_slot(data.slot)?,
Invalid::BadCommitteeIndex

View File

@@ -78,10 +78,10 @@ fn verify_exit_parametric<T: EthSpec>(
// Verify the validator has been active long enough.
verify!(
state.current_epoch() >= validator.activation_epoch + spec.persistent_committee_period,
state.current_epoch() >= validator.activation_epoch + spec.shard_committee_period,
ExitInvalid::TooYoungToExit {
current_epoch: state.current_epoch(),
earliest_exit_epoch: validator.activation_epoch + spec.persistent_committee_period,
earliest_exit_epoch: validator.activation_epoch + spec.shard_committee_period,
}
);

View File

@@ -51,11 +51,7 @@ pub fn process_rewards_and_penalties<T: EthSpec>(
return Err(Error::ValidatorStatusesInconsistent);
}
let mut deltas = vec![Delta::default(); state.balances.len()];
get_attestation_deltas(&mut deltas, state, &validator_statuses, spec)?;
get_proposer_deltas(&mut deltas, state, validator_statuses, spec)?;
let deltas = get_attestation_deltas(state, &validator_statuses, spec)?;
// Apply the deltas, erroring on overflow above but not on overflow below (saturating at 0
// instead).
@@ -67,78 +63,63 @@ pub fn process_rewards_and_penalties<T: EthSpec>(
Ok(())
}
/// For each attesting validator, reward the proposer who was first to include their attestation.
///
/// Spec v0.11.1
fn get_proposer_deltas<T: EthSpec>(
deltas: &mut Vec<Delta>,
state: &BeaconState<T>,
validator_statuses: &mut ValidatorStatuses,
spec: &ChainSpec,
) -> Result<(), Error> {
for (index, validator) in validator_statuses.statuses.iter().enumerate() {
if validator.is_previous_epoch_attester && !validator.is_slashed {
let inclusion = validator
.inclusion_info
.expect("It is a logic error for an attester not to have an inclusion delay.");
let base_reward = get_base_reward(
state,
index,
validator_statuses.total_balances.current_epoch(),
spec,
)?;
if inclusion.proposer_index >= deltas.len() {
return Err(Error::ValidatorStatusesInconsistent);
}
deltas[inclusion.proposer_index]
.reward(base_reward.safe_div(spec.proposer_reward_quotient)?)?;
}
}
Ok(())
}
/// Apply rewards for participation in attestations during the previous epoch.
///
/// Spec v0.11.1
fn get_attestation_deltas<T: EthSpec>(
deltas: &mut Vec<Delta>,
state: &BeaconState<T>,
validator_statuses: &ValidatorStatuses,
spec: &ChainSpec,
) -> Result<(), Error> {
) -> Result<Vec<Delta>, Error> {
let finality_delay = (state.previous_epoch() - state.finalized_checkpoint.epoch).as_u64();
for (index, validator) in validator_statuses.statuses.iter().enumerate() {
let base_reward = get_base_reward(
state,
index,
validator_statuses.total_balances.current_epoch(),
spec,
)?;
let mut deltas = vec![Delta::default(); state.validators.len()];
let delta = get_attestation_delta::<T>(
&validator,
&validator_statuses.total_balances,
base_reward,
finality_delay,
spec,
)?;
let total_balances = &validator_statuses.total_balances;
deltas[index].combine(delta)?;
// Filter out ineligible validators. All sub-functions of the spec do this except for
// `get_inclusion_delay_deltas`. It's safe to do so here because any validator that is in the
// unslashed indices of the matching source attestations is active, and therefore eligible.
for (index, validator) in validator_statuses
.statuses
.iter()
.enumerate()
.filter(|(_, validator)| is_eligible_validator(validator))
{
let base_reward = get_base_reward(state, index, total_balances.current_epoch(), spec)?;
let source_delta =
get_source_delta(validator, base_reward, total_balances, finality_delay, spec)?;
let target_delta =
get_target_delta(validator, base_reward, total_balances, finality_delay, spec)?;
let head_delta =
get_head_delta(validator, base_reward, total_balances, finality_delay, spec)?;
let (inclusion_delay_delta, proposer_delta) =
get_inclusion_delay_delta(validator, base_reward, spec)?;
let inactivity_penalty_delta =
get_inactivity_penalty_delta(validator, base_reward, finality_delay, spec)?;
deltas[index].combine(source_delta)?;
deltas[index].combine(target_delta)?;
deltas[index].combine(head_delta)?;
deltas[index].combine(inclusion_delay_delta)?;
deltas[index].combine(inactivity_penalty_delta)?;
if let Some((proposer_index, proposer_delta)) = proposer_delta {
if proposer_index >= deltas.len() {
return Err(Error::ValidatorStatusesInconsistent);
}
deltas[proposer_index].combine(proposer_delta)?;
}
}
Ok(())
Ok(deltas)
}
/// Determine the delta for a single validator, sans proposer rewards.
///
/// Spec v0.11.1
fn get_attestation_delta<T: EthSpec>(
validator: &ValidatorStatus,
fn get_attestation_component_delta(
index_in_unslashed_attesting_indices: bool,
attesting_balance: u64,
total_balances: &TotalBalances,
base_reward: u64,
finality_delay: u64,
@@ -146,83 +127,122 @@ fn get_attestation_delta<T: EthSpec>(
) -> Result<Delta, Error> {
let mut delta = Delta::default();
// Is this validator eligible to be rewarded or penalized?
// Spec: validator index in `eligible_validator_indices`
let is_eligible = validator.is_active_in_previous_epoch
|| (validator.is_slashed && !validator.is_withdrawable_in_current_epoch);
let total_balance = total_balances.current_epoch();
if !is_eligible {
return Ok(delta);
if index_in_unslashed_attesting_indices {
if finality_delay > spec.min_epochs_to_inactivity_penalty {
// Since full base reward will be canceled out by inactivity penalty deltas,
// optimal participation receives full base reward compensation here.
delta.reward(base_reward)?;
} else {
let reward_numerator = base_reward
.safe_mul(attesting_balance.safe_div(spec.effective_balance_increment)?)?;
delta.reward(
reward_numerator
.safe_div(total_balance.safe_div(spec.effective_balance_increment)?)?,
)?;
}
} else {
delta.penalize(base_reward)?;
}
// Handle integer overflow by dividing these quantities by EFFECTIVE_BALANCE_INCREMENT
// Spec:
// - increment = EFFECTIVE_BALANCE_INCREMENT
// - reward_numerator = get_base_reward(state, index) * (attesting_balance // increment)
// - rewards[index] = reward_numerator // (total_balance // increment)
let total_balance_ebi = total_balances
.current_epoch()
.safe_div(spec.effective_balance_increment)?;
let total_attesting_balance_ebi = total_balances
.previous_epoch_attesters()
.safe_div(spec.effective_balance_increment)?;
let matching_target_balance_ebi = total_balances
.previous_epoch_target_attesters()
.safe_div(spec.effective_balance_increment)?;
let matching_head_balance_ebi = total_balances
.previous_epoch_head_attesters()
.safe_div(spec.effective_balance_increment)?;
Ok(delta)
}
// Expected FFG source.
// Spec:
// - validator index in `get_unslashed_attesting_indices(state, matching_source_attestations)`
fn get_source_delta(
validator: &ValidatorStatus,
base_reward: u64,
total_balances: &TotalBalances,
finality_delay: u64,
spec: &ChainSpec,
) -> Result<Delta, Error> {
get_attestation_component_delta(
validator.is_previous_epoch_attester && !validator.is_slashed,
total_balances.previous_epoch_attesters(),
total_balances,
base_reward,
finality_delay,
spec,
)
}
fn get_target_delta(
validator: &ValidatorStatus,
base_reward: u64,
total_balances: &TotalBalances,
finality_delay: u64,
spec: &ChainSpec,
) -> Result<Delta, Error> {
get_attestation_component_delta(
validator.is_previous_epoch_target_attester && !validator.is_slashed,
total_balances.previous_epoch_target_attesters(),
total_balances,
base_reward,
finality_delay,
spec,
)
}
fn get_head_delta(
validator: &ValidatorStatus,
base_reward: u64,
total_balances: &TotalBalances,
finality_delay: u64,
spec: &ChainSpec,
) -> Result<Delta, Error> {
get_attestation_component_delta(
validator.is_previous_epoch_head_attester && !validator.is_slashed,
total_balances.previous_epoch_head_attesters(),
total_balances,
base_reward,
finality_delay,
spec,
)
}
fn get_inclusion_delay_delta(
validator: &ValidatorStatus,
base_reward: u64,
spec: &ChainSpec,
) -> Result<(Delta, Option<(usize, Delta)>), Error> {
// Spec: `index in get_unslashed_attesting_indices(state, matching_source_attestations)`
if validator.is_previous_epoch_attester && !validator.is_slashed {
delta.reward(
base_reward
.safe_mul(total_attesting_balance_ebi)?
.safe_div(total_balance_ebi)?,
)?;
// Inclusion speed bonus
let proposer_reward = base_reward.safe_div(spec.proposer_reward_quotient)?;
let max_attester_reward = base_reward.safe_sub(proposer_reward)?;
let inclusion = validator
let mut delta = Delta::default();
let mut proposer_delta = Delta::default();
let inclusion_info = validator
.inclusion_info
.expect("It is a logic error for an attester not to have an inclusion delay.");
delta.reward(max_attester_reward.safe_div(inclusion.delay)?)?;
} else {
delta.penalize(base_reward)?;
}
.ok_or(Error::ValidatorStatusesInconsistent)?;
// Expected FFG target.
// Spec:
// - validator index in `get_unslashed_attesting_indices(state, matching_target_attestations)`
if validator.is_previous_epoch_target_attester && !validator.is_slashed {
delta.reward(
base_reward
.safe_mul(matching_target_balance_ebi)?
.safe_div(total_balance_ebi)?,
)?;
} else {
delta.penalize(base_reward)?;
}
let proposer_reward = get_proposer_reward(base_reward, spec)?;
proposer_delta.reward(proposer_reward)?;
// Expected head.
// Spec:
// - validator index in `get_unslashed_attesting_indices(state, matching_head_attestations)`
if validator.is_previous_epoch_head_attester && !validator.is_slashed {
delta.reward(
base_reward
.safe_mul(matching_head_balance_ebi)?
.safe_div(total_balance_ebi)?,
)?;
let max_attester_reward = base_reward.safe_sub(proposer_reward)?;
delta.reward(max_attester_reward.safe_div(inclusion_info.delay)?)?;
let proposer_index = inclusion_info.proposer_index as usize;
Ok((delta, Some((proposer_index, proposer_delta))))
} else {
delta.penalize(base_reward)?;
Ok((Delta::default(), None))
}
}
fn get_inactivity_penalty_delta(
validator: &ValidatorStatus,
base_reward: u64,
finality_delay: u64,
spec: &ChainSpec,
) -> Result<Delta, Error> {
let mut delta = Delta::default();
// Inactivity penalty
if finality_delay > spec.min_epochs_to_inactivity_penalty {
// All eligible validators are penalized
delta.penalize(spec.base_rewards_per_epoch.safe_mul(base_reward)?)?;
// If validator is performing optimally this cancels all rewards for a neutral balance
delta.penalize(
spec.base_rewards_per_epoch
.safe_mul(base_reward)?
.safe_sub(get_proposer_reward(base_reward, spec)?)?,
)?;
// Additionally, all validators whose FFG target didn't match are penalized extra
// This condition is equivalent to this condition from the spec:
@@ -237,10 +257,20 @@ fn get_attestation_delta<T: EthSpec>(
}
}
// Proposer bonus is handled in `get_proposer_deltas`.
//
// This function only computes the delta for a single validator, so it cannot also return a
// delta for a validator.
Ok(delta)
}
/// Compute the reward awarded to a proposer for including an attestation from a validator.
///
/// The `base_reward` param should be the `base_reward` of the attesting validator.
fn get_proposer_reward(base_reward: u64, spec: &ChainSpec) -> Result<u64, Error> {
Ok(base_reward.safe_div(spec.proposer_reward_quotient)?)
}
/// Is the validator eligible for penalties and rewards at the current epoch?
///
/// Spec: v0.12.0
fn is_eligible_validator(validator: &ValidatorStatus) -> bool {
validator.is_active_in_previous_epoch
|| (validator.is_slashed && !validator.is_withdrawable_in_current_epoch)
}