def get_attestation_deltas(state: BeaconState, config: Eth2Config) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: committee_config = CommitteeConfig(config) rewards = tuple( 0 for _ in range(len(state.validators)) ) penalties = tuple( 0 for _ in range(len(state.validators)) ) previous_epoch = state.previous_epoch(config.SLOTS_PER_EPOCH, config.GENESIS_EPOCH) total_balance = get_total_active_balance(state, config) eligible_validator_indices = tuple( ValidatorIndex(index) for index, v in enumerate(state.validators) if v.is_active(previous_epoch) or ( v.slashed and previous_epoch + 1 < v.withdrawable_epoch ) ) matching_source_attestations = get_matching_source_attestations( state, previous_epoch, config, ) matching_target_attestations = get_matching_target_attestations( state, previous_epoch, config, ) matching_head_attestations = get_matching_head_attestations( state, previous_epoch, config, ) for attestations in ( matching_source_attestations, matching_target_attestations, matching_head_attestations ): unslashed_attesting_indices = get_unslashed_attesting_indices( state, attestations, committee_config, ) attesting_balance = get_total_balance(state, unslashed_attesting_indices) for index in eligible_validator_indices: if index in unslashed_attesting_indices: rewards = update_tuple_item_with_fn( rewards, index, lambda balance, delta: balance + delta, get_base_reward( state, index, config, ) * attesting_balance // total_balance, ) else: penalties = update_tuple_item_with_fn( penalties, index, lambda balance, delta: balance + delta, get_base_reward( state, index, config, ), ) for index in get_unslashed_attesting_indices( state, matching_source_attestations, committee_config, ): attestation = min( ( a for a in matching_source_attestations if index in get_attesting_indices( state, a.data, a.aggregation_bitfield, committee_config, ) ), key=lambda a: a.inclusion_delay, ) base_reward = get_base_reward(state, index, config) proposer_reward = base_reward // config.PROPOSER_REWARD_QUOTIENT rewards = update_tuple_item_with_fn( rewards, attestation.proposer_index, lambda balance, delta: balance + delta, proposer_reward, ) max_attester_reward = base_reward - proposer_reward rewards = update_tuple_item_with_fn( rewards, index, lambda balance, delta: balance + delta, ( max_attester_reward * config.MIN_ATTESTATION_INCLUSION_DELAY // attestation.inclusion_delay ) ) finality_delay = previous_epoch - state.finalized_epoch if finality_delay > config.MIN_EPOCHS_TO_INACTIVITY_PENALTY: matching_target_attesting_indices = get_unslashed_attesting_indices( state, matching_target_attestations, committee_config, ) for index in eligible_validator_indices: penalties = update_tuple_item_with_fn( penalties, index, lambda balance, delta: balance + delta, BASE_REWARDS_PER_EPOCH * get_base_reward( state, index, config, ), ) if index not in matching_target_attesting_indices: effective_balance = state.validators[index].effective_balance penalties = update_tuple_item_with_fn( penalties, index, lambda balance, delta: balance + delta, effective_balance * finality_delay // config.INACTIVITY_PENALTY_QUOTIENT, ) return tuple( Gwei(reward) for reward in rewards ), tuple( Gwei(penalty) for penalty in penalties )
def _compute_next_start_shard(state: BeaconState, config: Eth2Config) -> Shard: current_epoch = state.current_epoch(config.SLOTS_PER_EPOCH) return ( state.start_shard + get_shard_delta(state, current_epoch, CommitteeConfig(config)) ) % config.SHARD_COUNT
def test_slash_validator(genesis_state, config): some_epoch = (config.GENESIS_EPOCH + random.randrange(1, 2**32) + config.EPOCHS_PER_SLASHINGS_VECTOR) earliest_slashable_epoch = some_epoch - config.EPOCHS_PER_SLASHINGS_VECTOR slashable_range = range(earliest_slashable_epoch, some_epoch) sampling_quotient = 4 state = genesis_state.copy(slot=compute_start_slot_at_epoch( earliest_slashable_epoch, config.SLOTS_PER_EPOCH)) validator_count_to_slash = len(state.validators) // sampling_quotient assert validator_count_to_slash > 1 validator_indices_to_slash = random.sample(range(len(state.validators)), validator_count_to_slash) # ensure case w/ one slashing in an epoch # by ignoring the first set_of_colluding_validators = validator_indices_to_slash[1:] # simulate multiple slashings in an epoch validators_grouped_by_coalition = groupby( lambda index: index % sampling_quotient, set_of_colluding_validators) coalition_count = len(validators_grouped_by_coalition) slashings = { epoch: coalition for coalition, epoch in zip( validators_grouped_by_coalition.values(), random.sample(slashable_range, coalition_count), ) } another_slashing_epoch = first(random.sample(slashable_range, 1)) while another_slashing_epoch in slashings: another_slashing_epoch += 1 slashings[another_slashing_epoch] = (validator_indices_to_slash[0], ) expected_slashings = {} expected_individual_penalties = {} for epoch, coalition in slashings.items(): for index in coalition: validator = state.validators[index] assert validator.is_active(earliest_slashable_epoch) assert validator.exit_epoch == FAR_FUTURE_EPOCH assert validator.withdrawable_epoch == FAR_FUTURE_EPOCH expected_slashings = update_in( expected_slashings, [epoch], lambda balance: balance + state.validators[index]. effective_balance, default=0, ) expected_individual_penalties = update_in( expected_individual_penalties, [index], lambda penalty: (penalty + (state.validators[index].effective_balance // config .MIN_SLASHING_PENALTY_QUOTIENT)), default=0, ) # emulate slashings across the current slashable range expected_proposer_rewards = {} for epoch, coalition in slashings.items(): state = state.copy( slot=compute_start_slot_at_epoch(epoch, config.SLOTS_PER_EPOCH)) expected_total_slashed_balance = expected_slashings[epoch] proposer_index = get_beacon_proposer_index(state, CommitteeConfig(config)) expected_proposer_rewards = update_in( expected_proposer_rewards, [proposer_index], lambda reward: reward + (expected_total_slashed_balance // config. WHISTLEBLOWER_REWARD_QUOTIENT), default=0, ) for index in coalition: state = slash_validator(state, index, config) state = state.copy( slot=compute_start_slot_at_epoch(some_epoch, config.SLOTS_PER_EPOCH)) # verify result for epoch, coalition in slashings.items(): for index in coalition: validator = state.validators[index] assert validator.exit_epoch != FAR_FUTURE_EPOCH assert validator.slashed assert validator.withdrawable_epoch == max( validator.exit_epoch + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY, epoch + config.EPOCHS_PER_SLASHINGS_VECTOR, ) slashed_epoch_index = epoch % config.EPOCHS_PER_SLASHINGS_VECTOR slashed_balance = state.slashings[slashed_epoch_index] assert slashed_balance == expected_slashings[epoch] assert state.balances[index] == ( config.MAX_EFFECTIVE_BALANCE - expected_individual_penalties[index] + expected_proposer_rewards.get(index, 0)) for proposer_index, total_reward in expected_proposer_rewards.items(): assert state.balances[proposer_index] == ( total_reward + config.MAX_EFFECTIVE_BALANCE - expected_individual_penalties.get(proposer_index, 0))
def _get_proposer_index(state: BeaconState, config: Eth2Config) -> ValidatorIndex: proposer_index = get_beacon_proposer_index(state, CommitteeConfig(config)) return proposer_index
def on_attestation(self, attestation: Attestation, validate_signature: bool = True) -> None: target = attestation.data.target current_epoch = compute_epoch_at_slot(self.get_current_slot(), self._config.SLOTS_PER_EPOCH) previous_epoch = (current_epoch - 1 if current_epoch > self._config.GENESIS_EPOCH else self._config.GENESIS_EPOCH) if target.epoch not in (current_epoch, previous_epoch): raise ValidationError( "Attestations must be from the current or previous epoch") if target.root not in self._context.blocks: raise ValidationError( "Attestation targets a block we have not seen") base_state = self._context.block_states[target.root] time_of_target_epoch = ( base_state.genesis_time + compute_start_slot_at_epoch( target.epoch, self._config.SLOTS_PER_EPOCH) * self._config.SECONDS_PER_SLOT) if self._context.time < time_of_target_epoch: raise ValidationError("Attestation cannot be for a future epoch") beacon_block_root = attestation.data.beacon_block_root if beacon_block_root not in self._context.blocks: raise ValidationError("Attestations must be for a known block") if self._context.blocks[beacon_block_root].slot > attestation.data.slot: raise ValidationError( "Attestations must not be for a block in the future") if target not in self._context.checkpoint_states: base_state = process_slots( base_state, compute_start_slot_at_epoch(target.epoch, self._config.SLOTS_PER_EPOCH), self._config, ) self._context.checkpoint_states[target] = base_state target_state = self._context.checkpoint_states[target] if (self._context.time < (attestation.data.slot + 1) * self._config.SECONDS_PER_SLOT): raise ValidationError( "Attestations can only affect the fork choice of future slots") # TODO: has this validation already been performed? indexed_attestation = get_indexed_attestation( target_state, attestation, CommitteeConfig(self._config)) validate_indexed_attestation( target_state, indexed_attestation, self._config.MAX_VALIDATORS_PER_COMMITTEE, self._config.SLOTS_PER_EPOCH, validate_signature=validate_signature, ) for i in indexed_attestation.attesting_indices: if (i not in self._context.latest_messages or target.epoch > self._context.latest_messages[i].epoch): self._context.latest_messages[i] = LatestMessage( epoch=target.epoch, root=attestation.data.beacon_block_root)
async def handle_first_tick(self, slot: Slot) -> None: head = self.chain.get_canonical_head() state_machine = self.chain.get_state_machine() state = self.chain.get_head_state() self.logger.debug( bold_green( "status at slot %s in epoch %s: state_root %s, finalized_checkpoint %s" ), state.slot, state.current_epoch(self.slots_per_epoch), humanize_hash(head.state_root), state.finalized_checkpoint, ) self.logger.debug( ( "status at slot %s in epoch %s:" " previous_justified_checkpoint %s, current_justified_checkpoint %s" ), state.slot, state.current_epoch(self.slots_per_epoch), state.previous_justified_checkpoint, state.current_justified_checkpoint, ) self.logger.debug( ( "status at slot %s in epoch %s:" " previous_epoch_attestations %s, current_epoch_attestations %s" ), state.slot, state.current_epoch(self.slots_per_epoch), state.previous_epoch_attestations, state.current_epoch_attestations, ) # To see if a validator is assigned to propose during the slot, the beacon state must # be in the epoch in question. At the epoch boundaries, the validator must run an # epoch transition into the epoch to successfully check the proposal assignment of the # first slot. temp_state = state_machine.state_transition.apply_state_transition( state, future_slot=slot, ) proposer_index = get_beacon_proposer_index( temp_state, CommitteeConfig(state_machine.config), ) # `latest_proposed_epoch` is used to prevent validator from erraneously proposing twice # in the same epoch due to service crashing. epoch = compute_epoch_at_slot(slot, self.slots_per_epoch) if proposer_index in self.validator_privkeys: has_proposed = epoch <= self.latest_proposed_epoch[proposer_index] if not has_proposed: await self.propose_block( proposer_index=proposer_index, slot=slot, state=state, state_machine=state_machine, head_block=head, ) self.latest_proposed_epoch[proposer_index] = epoch
def test_get_attestation_deltas( genesis_state, config, slots_per_epoch, target_committee_size, shard_count, min_attestation_inclusion_delay, inactivity_penalty_quotient, finalized_epoch, current_slot, sample_pending_attestation_record_params, sample_attestation_data_params, ): state = genesis_state.copy( slot=current_slot, finalized_checkpoint=Checkpoint(epoch=finalized_epoch)) previous_epoch = state.previous_epoch(config.SLOTS_PER_EPOCH, config.GENESIS_EPOCH) epoch_start_shard = get_start_shard(state, previous_epoch, CommitteeConfig(config)) shard_delta = get_shard_delta(state, previous_epoch, CommitteeConfig(config)) a = epoch_start_shard b = epoch_start_shard + shard_delta if a > b: valid_shards_for_epoch = range(b, a) else: valid_shards_for_epoch = range(a, b) indices_to_check = set() prev_epoch_start_slot = compute_start_slot_of_epoch( previous_epoch, slots_per_epoch) prev_epoch_attestations = tuple() for slot in range(prev_epoch_start_slot, prev_epoch_start_slot + slots_per_epoch): committee, shard = get_crosslink_committees_at_slot( state, slot, CommitteeConfig(config))[0] if not committee: continue if shard not in valid_shards_for_epoch: continue participants_bitfield = get_empty_bitfield(len(committee)) for i, index in enumerate(committee): indices_to_check.add(index) participants_bitfield = set_voted(participants_bitfield, i) prev_epoch_attestations += (PendingAttestation( **sample_pending_attestation_record_params).copy( aggregation_bits=participants_bitfield, inclusion_delay=min_attestation_inclusion_delay, proposer_index=get_beacon_proposer_index( state.copy(slot=slot), CommitteeConfig(config)), data=AttestationData(**sample_attestation_data_params).copy( crosslink=Crosslink(shard=shard), target=Checkpoint( epoch=previous_epoch, root=get_block_root( state, previous_epoch, config.SLOTS_PER_EPOCH, config.SLOTS_PER_HISTORICAL_ROOT, ), ), beacon_block_root=get_block_root_at_slot( state, slot, config.SLOTS_PER_HISTORICAL_ROOT), ), ), ) state = state.copy(previous_epoch_attestations=prev_epoch_attestations) rewards_received, penalties_received = get_attestation_deltas( state, config) # everyone attested, no penalties assert sum(penalties_received) == 0 the_reward = rewards_received[0] # everyone performed the same, equal rewards assert sum(rewards_received) // len(rewards_received) == the_reward
def test_find_winning_crosslink_and_attesting_indices_from_candidates( genesis_state, number_of_candidates, config): some_epoch = config.GENESIS_EPOCH + 20 some_shard = 3 state = genesis_state.copy( slot=get_epoch_start_slot(some_epoch, config.SLOTS_PER_EPOCH), start_shard=some_shard, current_crosslinks=tuple( Crosslink( shard=i, data_root=(i).to_bytes(32, "little"), ) for i in range(config.SHARD_COUNT)), ) full_committee = get_crosslink_committee( state, some_epoch, some_shard, CommitteeConfig(config), ) # break the committees up into different subsets to simulate different # attestations for the same crosslink committees = tuple( random_sample( len(full_committee) // number_of_candidates, full_committee) for _ in range(number_of_candidates)) seen = set() filtered_committees = tuple() for committee in committees: deduplicated_committee = tuple() for index in committee: if index in seen: pass else: seen.add(index) deduplicated_committee += (index, ) filtered_committees += (deduplicated_committee, ) candidates = tuple( mk_pending_attestation_from_committee( state.current_crosslinks[some_shard], len(full_committee), some_shard, target_epoch=some_epoch, ) for committee in filtered_committees) if number_of_candidates == 0: expected_result = (Crosslink(), tuple()) else: expected_result = ( candidates[0].data.crosslink, tuple(sorted(full_committee)), ) result = _find_winning_crosslink_and_attesting_indices_from_candidates( state, candidates, config, ) assert result == expected_result
def test_find_candidate_attestations_for_shard(genesis_state, config): some_epoch = config.GENESIS_EPOCH + 20 # start on some shard and walk a subset of them some_shard = 3 shard_offset = 24 state = genesis_state.copy( slot=compute_start_slot_of_epoch(some_epoch, config.SLOTS_PER_EPOCH), start_shard=some_shard, current_crosslinks=tuple( Crosslink(shard=i, data_root=(i).to_bytes(32, "little")) for i in range(config.SHARD_COUNT)), ) # sample a subset of the shards to make attestations for some_shards_with_attestations = random.sample( range(some_shard, some_shard + shard_offset), shard_offset // 2) committee_and_shard_pairs = tuple( ( get_crosslink_committee(state, some_epoch, some_shard + i, CommitteeConfig(config)), some_shard + i, ) for i in range(shard_offset) if some_shard + i in some_shards_with_attestations) pending_attestations = { shard: mk_pending_attestation_from_committee(state.current_crosslinks[shard], len(committee), shard) for committee, shard in committee_and_shard_pairs } # invalidate some crosslinks to test the crosslink filter some_crosslinks_to_mangle = random.sample( some_shards_with_attestations, len(some_shards_with_attestations) // 2) shards_with_valid_crosslinks = set(some_shards_with_attestations) - set( some_crosslinks_to_mangle) crosslinks = tuple() for shard in range(config.SHARD_COUNT): if shard in shards_with_valid_crosslinks: crosslinks += (state.current_crosslinks[shard], ) else: crosslinks += (Crosslink(), ) state = state.copy(current_crosslinks=crosslinks) # check around the range of shards we built up for shard in range(0, some_shard + shard_offset + 3): if shard in some_shards_with_attestations: attestations = _get_attestations_for_shard( pending_attestations.values(), shard) assert attestations == (pending_attestations[shard], ) if shard in some_crosslinks_to_mangle: assert not _get_attestations_for_valid_crosslink( pending_attestations.values(), state, shard, config) else: attestations = _get_attestations_for_valid_crosslink( pending_attestations.values(), state, shard, config) assert attestations == (pending_attestations[shard], ) else: assert not _get_attestations_for_shard( pending_attestations.values(), shard) assert not _get_attestations_for_valid_crosslink( pending_attestations.values(), state, shard, config)
def _process_rewards_and_penalties_for_finality( state: BeaconState, config: Eth2Config, previous_epoch_active_validator_indices: Sequence[ValidatorIndex], previous_total_balance: Gwei, previous_epoch_attestations: Sequence[Attestation], previous_epoch_attester_indices: Sequence[ValidatorIndex], inclusion_infos: Dict[ValidatorIndex, InclusionInfo], effective_balances: Dict[ValidatorIndex, Gwei], base_rewards: Dict[ValidatorIndex, Gwei] ) -> Tuple[Dict[ValidatorIndex, Gwei], Dict[ValidatorIndex, Gwei]]: # noqa: E501 previous_epoch_boundary_attestations = get_previous_epoch_boundary_attestations( state, config.SLOTS_PER_EPOCH, config.SLOTS_PER_HISTORICAL_ROOT, ) previous_epoch_boundary_attester_indices = get_attester_indices_from_attestations( state=state, attestations=previous_epoch_boundary_attestations, committee_config=CommitteeConfig(config), ) previous_epoch_head_attestations = get_previous_epoch_matching_head_attestations( state, config.SLOTS_PER_EPOCH, config.SLOTS_PER_HISTORICAL_ROOT, ) previous_epoch_head_attester_indices = get_attester_indices_from_attestations( state=state, attestations=previous_epoch_head_attestations, committee_config=CommitteeConfig(config), ) epochs_since_finality = state.next_epoch( config.SLOTS_PER_EPOCH) - state.finalized_epoch if epochs_since_finality <= 4: return _compute_normal_justification_and_finalization_deltas( state, config, previous_epoch_active_validator_indices, previous_total_balance, previous_epoch_attester_indices, previous_epoch_boundary_attester_indices, previous_epoch_head_attester_indices, inclusion_infos, effective_balances, base_rewards, ) # epochs_since_finality > 4 else: return _compute_inactivity_leak_deltas( state, config, previous_epoch_active_validator_indices, previous_epoch_attester_indices, previous_epoch_boundary_attester_indices, previous_epoch_head_attester_indices, inclusion_infos, effective_balances, base_rewards, epochs_since_finality, )
def process_rewards_and_penalties(state: BeaconState, config: Eth2Config) -> BeaconState: # Compute previous epoch active validator indices and the total balance they account for # for later use. previous_epoch_active_validator_indices = set( get_active_validator_indices( state.validator_registry, state.previous_epoch(config.SLOTS_PER_EPOCH))) previous_total_balance: Gwei = get_total_balance( state.validator_balances, tuple(previous_epoch_active_validator_indices), config.MAX_DEPOSIT_AMOUNT, ) # Compute previous epoch attester indices and the total balance they account for # for later use. previous_epoch_attestations = state.previous_epoch_attestations previous_epoch_attester_indices = get_attester_indices_from_attestations( state=state, attestations=previous_epoch_attestations, committee_config=CommitteeConfig(config), ) # Compute inclusion slot/distance of previous attestations for later use. inclusion_infos = get_inclusion_infos( state=state, attestations=previous_epoch_attestations, committee_config=CommitteeConfig(config), ) # Compute effective balance of each previous epoch active validator for later use effective_balances = { ValidatorIndex(index): get_effective_balance( state.validator_balances, ValidatorIndex(index), config.MAX_DEPOSIT_AMOUNT, ) for index in range(len(state.validator_registry)) } # Compute base reward of each previous epoch active validator for later use base_rewards = { ValidatorIndex(index): get_base_reward( state=state, index=ValidatorIndex(index), base_reward_quotient=config.BASE_REWARD_QUOTIENT, previous_total_balance=previous_total_balance, max_deposit_amount=config.MAX_DEPOSIT_AMOUNT, ) for index in range(len(state.validator_registry)) } # 1. Process rewards and penalties for justification and finalization finality_rewards, finality_penalties = _process_rewards_and_penalties_for_finality( state, config, previous_epoch_active_validator_indices, previous_total_balance, previous_epoch_attestations, previous_epoch_attester_indices, inclusion_infos, effective_balances, base_rewards, ) # 2. Process rewards and penalties for crosslinks crosslinks_rewards, crosslinks_penalties = _process_rewards_and_penalties_for_crosslinks( state, config, effective_balances, base_rewards, ) # Apply the overall rewards/penalties for index in range(len(state.validator_registry)): state = state.update_validator_balance( ValidatorIndex(index), # Prevent validator balance under flow max( (state.validator_balances[index] + finality_rewards[index] + crosslinks_rewards[index] - finality_penalties[index] - crosslinks_penalties[index]), 0, ), ) return state
def _compute_normal_justification_and_finalization_deltas( state: BeaconState, config: Eth2Config, previous_epoch_active_validator_indices: Sequence[ValidatorIndex], previous_total_balance: Gwei, previous_epoch_attester_indices: Sequence[ValidatorIndex], previous_epoch_boundary_attester_indices: Sequence[ValidatorIndex], previous_epoch_head_attester_indices: Sequence[ValidatorIndex], inclusion_infos: Dict[ValidatorIndex, InclusionInfo], effective_balances: Dict[ValidatorIndex, Gwei], base_rewards: Dict[ValidatorIndex, Gwei] ) -> Tuple[Dict[ValidatorIndex, Gwei], Dict[ValidatorIndex, Gwei]]: # noqa: E501 rewards_received = { ValidatorIndex(index): Gwei(0) for index in range(len(state.validator_registry)) } penalties_received = rewards_received.copy() previous_epoch_attesting_balance = get_total_balance_from_effective_balances( effective_balances, previous_epoch_attester_indices, ) previous_epoch_boundary_attesting_balance = get_total_balance_from_effective_balances( effective_balances, previous_epoch_boundary_attester_indices, ) previous_epoch_head_attesting_balance = get_total_balance_from_effective_balances( effective_balances, previous_epoch_head_attester_indices, ) for index in previous_epoch_active_validator_indices: # Expected FFG source if index in previous_epoch_attester_indices: rewards_received = _update_rewards_or_penalies( index, base_rewards[index] * previous_epoch_attesting_balance // previous_total_balance, rewards_received, ) # Inclusion speed bonus rewards_received = _update_rewards_or_penalies( index, (base_rewards[index] * config.MIN_ATTESTATION_INCLUSION_DELAY // inclusion_infos[index].inclusion_distance), rewards_received, ) else: penalties_received = _update_rewards_or_penalies( index, base_rewards[index], penalties_received, ) # Expected FFG target if index in previous_epoch_boundary_attester_indices: rewards_received = _update_rewards_or_penalies( index, (base_rewards[index] * previous_epoch_boundary_attesting_balance // previous_total_balance), rewards_received, ) else: penalties_received = _update_rewards_or_penalies( index, base_rewards[index], penalties_received, ) # Expected head if index in previous_epoch_head_attester_indices: rewards_received = _update_rewards_or_penalies( index, (base_rewards[index] * previous_epoch_head_attesting_balance // previous_total_balance), rewards_received, ) else: penalties_received = _update_rewards_or_penalies( index, base_rewards[index], penalties_received, ) # Proposer bonus if index in previous_epoch_attester_indices: proposer_index = get_beacon_proposer_index( state, inclusion_infos[index].inclusion_slot, CommitteeConfig(config), ) rewards_received = _update_rewards_or_penalies( proposer_index, base_rewards[index] // config.ATTESTATION_INCLUSION_REWARD_QUOTIENT, rewards_received, ) return (rewards_received, penalties_received)
def process_crosslinks(state: BeaconState, config: Eth2Config) -> BeaconState: """ Implement 'per-epoch-processing.crosslinks' portion of Phase 0 spec: https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#crosslinks For each shard from the past two epochs, find the shard block root that has been attested to by the most stake. If enough(>= 2/3 total stake) attesting stake, update the crosslink record of that shard. Return resulting ``state`` """ latest_crosslinks = state.latest_crosslinks effective_balances = { ValidatorIndex(index): get_effective_balance( state.validator_balances, ValidatorIndex(index), config.MAX_DEPOSIT_AMOUNT, ) for index in range(len(state.validator_registry)) } previous_epoch_start_slot = get_epoch_start_slot( state.previous_epoch(config.SLOTS_PER_EPOCH), config.SLOTS_PER_EPOCH, ) next_epoch_start_slot = get_epoch_start_slot( state.next_epoch(config.SLOTS_PER_EPOCH), config.SLOTS_PER_EPOCH, ) for slot in range(previous_epoch_start_slot, next_epoch_start_slot): crosslink_committees_at_slot = get_crosslink_committees_at_slot( state, slot, CommitteeConfig(config), ) for crosslink_committee, shard in crosslink_committees_at_slot: winning_root, attesting_validator_indices = get_winning_root_and_participants( state=state, shard=shard, effective_balances=effective_balances, committee_config=CommitteeConfig(config), ) if len(attesting_validator_indices) > 0: total_attesting_balance = get_total_balance( state.validator_balances, attesting_validator_indices, config.MAX_DEPOSIT_AMOUNT, ) total_balance = get_total_balance( state.validator_balances, crosslink_committee, config.MAX_DEPOSIT_AMOUNT, ) if 3 * total_attesting_balance >= 2 * total_balance: latest_crosslinks = update_tuple_item( latest_crosslinks, shard, CrosslinkRecord( epoch=slot_to_epoch(Slot(slot), config.SLOTS_PER_EPOCH), crosslink_data_root=winning_root, ), ) state = state.copy(latest_crosslinks=latest_crosslinks, ) return state
def test_process_rewards_and_penalties_for_crosslinks( genesis_state, config, slots_per_epoch, target_committee_size, shard_count, current_slot, num_attesting_validators, max_effective_balance, min_attestation_inclusion_delay, sample_attestation_data_params, sample_pending_attestation_record_params, ): state = genesis_state.copy(slot=current_slot) previous_epoch = state.previous_epoch(config.SLOTS_PER_EPOCH, config.GENESIS_EPOCH) prev_epoch_start_slot = compute_start_slot_of_epoch( previous_epoch, slots_per_epoch) prev_epoch_crosslink_committees = [ get_crosslink_committees_at_slot(state, slot, CommitteeConfig(config))[0] for slot in range(prev_epoch_start_slot, prev_epoch_start_slot + slots_per_epoch) ] # Record which validators attest during each slot for reward collation. each_slot_attestion_validators_list = [] epoch_start_shard = get_start_shard(state, previous_epoch, CommitteeConfig(config)) shard_delta = get_shard_delta(state, previous_epoch, CommitteeConfig(config)) a = epoch_start_shard b = epoch_start_shard + shard_delta if a > b: valid_shards_for_epoch = range(b, a) else: valid_shards_for_epoch = range(a, b) indices_to_check = set() previous_epoch_attestations = [] for committee, shard in prev_epoch_crosslink_committees: if shard not in valid_shards_for_epoch: continue for index in committee: indices_to_check.add(index) # Randomly sample `num_attesting_validators` validators # from the committee to attest in this slot. crosslink_attesting_validators = random.sample( committee, num_attesting_validators) each_slot_attestion_validators_list.append( crosslink_attesting_validators) participants_bitfield = get_empty_bitfield(len(committee)) for index in crosslink_attesting_validators: participants_bitfield = set_voted(participants_bitfield, committee.index(index)) previous_epoch_attestations.append( PendingAttestation( **sample_pending_attestation_record_params).copy( aggregation_bits=participants_bitfield, data=AttestationData( **sample_attestation_data_params).copy( target=Checkpoint(epoch=previous_epoch), crosslink=Crosslink( shard=shard, parent_root=Crosslink().hash_tree_root), ), )) state = state.copy( previous_epoch_attestations=tuple(previous_epoch_attestations)) rewards_received, penalties_received = get_crosslink_deltas(state, config) expected_rewards_received = { index: 0 for index in range(len(state.validators)) } validator_balance = max_effective_balance for i in range(slots_per_epoch): crosslink_committee, shard = prev_epoch_crosslink_committees[i] if shard not in valid_shards_for_epoch: continue attesting_validators = each_slot_attestion_validators_list[i] total_attesting_balance = len(attesting_validators) * validator_balance total_committee_balance = len(crosslink_committee) * validator_balance for index in crosslink_committee: if index in attesting_validators: reward = ( get_base_reward(state=state, index=index, config=config) * total_attesting_balance // total_committee_balance) expected_rewards_received[index] += reward else: penalty = get_base_reward(state=state, index=index, config=config) expected_rewards_received[index] -= penalty # Check the rewards/penalties match for index in range(len(state.validators)): if index not in indices_to_check: continue assert (rewards_received[index] - penalties_received[index] == expected_rewards_received[index])
def get_crosslink_deltas(state: BeaconState, config: Eth2Config) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: rewards = tuple( 0 for _ in range(len(state.validators)) ) penalties = tuple( 0 for _ in range(len(state.validators)) ) epoch = state.previous_epoch(config.SLOTS_PER_EPOCH, config.GENESIS_EPOCH) active_validators_indices = get_active_validator_indices(state.validators, epoch) epoch_committee_count = get_epoch_committee_count( len(active_validators_indices), config.SHARD_COUNT, config.SLOTS_PER_EPOCH, config.TARGET_COMMITTEE_SIZE, ) epoch_start_shard = get_epoch_start_shard( state, epoch, CommitteeConfig(config), ) for shard_offset in range(epoch_committee_count): shard = Shard((epoch_start_shard + shard_offset) % config.SHARD_COUNT) crosslink_committee = get_crosslink_committee( state, epoch, shard, CommitteeConfig(config), ) _, attesting_indices = get_winning_crosslink_and_attesting_indices( state=state, epoch=epoch, shard=shard, config=config, ) total_attesting_balance = get_total_balance( state, attesting_indices, ) total_committee_balance = get_total_balance( state, crosslink_committee, ) for index in crosslink_committee: base_reward = get_base_reward(state, index, config) if index in attesting_indices: rewards = update_tuple_item_with_fn( rewards, index, lambda balance, delta: balance + delta, base_reward * total_attesting_balance // total_committee_balance ) else: penalties = update_tuple_item_with_fn( penalties, index, lambda balance, delta: balance + delta, base_reward, ) return tuple( Gwei(reward) for reward in rewards ), tuple( Gwei(penalty) for penalty in penalties )
def process_attestations(state: BeaconState, block: BaseBeaconBlock, config: Eth2Config) -> BeaconState: """ Implements 'per-block-processing.operations.attestations' portion of Phase 0 spec: https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#attestations-1 Validate the ``attestations`` contained within the ``block`` in the context of ``state``. If any invalid, throw ``ValidationError``. Otherwise, append a ``PendingAttestationRecords`` for each to ``previous_epoch_attestations`` or ``current_epoch_attestations``. Return resulting ``state``. """ if len(block.body.attestations) > config.MAX_ATTESTATIONS: raise ValidationError( f"The block ({block}) has too many attestations:\n" f"\tFound {len(block.body.attestations)} attestations, " f"maximum: {config.MAX_ATTESTATIONS}" ) for attestation in block.body.attestations: validate_attestation( state, attestation, config.MIN_ATTESTATION_INCLUSION_DELAY, config.SLOTS_PER_HISTORICAL_ROOT, CommitteeConfig(config), ) # update attestations previous_epoch = state.previous_epoch(config.SLOTS_PER_EPOCH, config.GENESIS_EPOCH) current_epoch = state.current_epoch(config.SLOTS_PER_EPOCH) new_previous_epoch_pending_attestations = [] new_current_epoch_pending_attestations = [] for attestation in block.body.attestations: if slot_to_epoch(attestation.data.slot, config.SLOTS_PER_EPOCH) == current_epoch: new_current_epoch_pending_attestations.append( PendingAttestationRecord( data=attestation.data, aggregation_bitfield=attestation.aggregation_bitfield, custody_bitfield=attestation.custody_bitfield, slot_included=state.slot, ) ) elif slot_to_epoch(attestation.data.slot, config.SLOTS_PER_EPOCH) == previous_epoch: new_previous_epoch_pending_attestations.append( PendingAttestationRecord( data=attestation.data, aggregation_bitfield=attestation.aggregation_bitfield, custody_bitfield=attestation.custody_bitfield, slot_included=state.slot, ) ) state = state.copy( previous_epoch_attestations=( state.previous_epoch_attestations + tuple(new_previous_epoch_pending_attestations) ), current_epoch_attestations=( state.current_epoch_attestations + tuple(new_current_epoch_pending_attestations) ), ) return state
def committee_config(config): return CommitteeConfig(config)
def test_get_attestation_deltas( genesis_state, config, slots_per_epoch, target_committee_size, max_committees_per_slot, min_attestation_inclusion_delay, inactivity_penalty_quotient, finalized_epoch, current_slot, sample_pending_attestation_record_params, sample_attestation_data_params, ): state = genesis_state.mset( "slot", current_slot, "finalized_checkpoint", Checkpoint.create(epoch=finalized_epoch), ) previous_epoch = state.previous_epoch(config.SLOTS_PER_EPOCH, config.GENESIS_EPOCH) has_inactivity_penalty = (previous_epoch - finalized_epoch > config.MIN_EPOCHS_TO_INACTIVITY_PENALTY) indices_to_check = set() prev_epoch_attestations = tuple() for committee, committee_index, slot in iterate_committees_at_epoch( state, previous_epoch, config): participants_bitfield = get_empty_bitfield(len(committee)) for i, index in enumerate(committee): indices_to_check.add(index) participants_bitfield = set_voted(participants_bitfield, i) prev_epoch_attestations += (PendingAttestation.create( **sample_pending_attestation_record_params).mset( "aggregation_bits", participants_bitfield, "inclusion_delay", min_attestation_inclusion_delay, "proposer_index", get_beacon_proposer_index(state.set("slot", slot), CommitteeConfig(config)), "data", AttestationData.create(**sample_attestation_data_params).mset( "slot", slot, "index", committee_index, "beacon_block_root", get_block_root_at_slot(state, slot, config.SLOTS_PER_HISTORICAL_ROOT), "target", Checkpoint.create( epoch=previous_epoch, root=get_block_root( state, previous_epoch, config.SLOTS_PER_EPOCH, config.SLOTS_PER_HISTORICAL_ROOT, ), ), ), ), ) state = state.set("previous_epoch_attestations", prev_epoch_attestations) rewards_received, penalties_received = get_attestation_deltas( state, config) if has_inactivity_penalty: assert sum(penalties_received) > 0 else: assert sum(penalties_received) == 0 assert all(reward > 0 for reward in rewards_received)
async def aggregate( self, slot: Slot ) -> Tuple[AggregateAndProof, ...]: """ Aggregate the attestations at ``slot`` and broadcast them. """ # Check the aggregators selection aggregate_and_proofs: Tuple[AggregateAndProof, ...] = () state_machine = self.chain.get_state_machine() state = self.chain.get_head_state() config = state_machine.config attesting_committee_assignments_at_slot = self._get_attesting_assignments_at_slot(slot) # 1. For each committee_assignment at the given slot for committee_assignment in attesting_committee_assignments_at_slot: committee_index = committee_assignment.committee_index local_attesters = self._get_local_attesters_at_assignment(committee_assignment) # Get the validator_index -> privkey map of the attesting validators attesting_validator_privkeys = { index: self.validator_privkeys[index] for index in local_attesters } selected_proofs: Dict[ValidatorIndex, BLSSignature] = {} # 2. For each attester for validator_index, privkey in attesting_validator_privkeys.items(): # Check if the vallidator is one of the aggregators signature = get_slot_signature( state, slot, privkey, CommitteeConfig(config), ) is_aggregator_result = is_aggregator( state, slot, committee_index, signature, CommitteeConfig(config), ) if is_aggregator_result: self.logger.debug( f"validator ({validator_index}) is aggregator of" f" committee_index={committee_index} at slot={slot}" ) selected_proofs[validator_index] = signature else: continue aggregates = self._get_aggregates(slot, committee_index, config) # 3. For each aggregate # (it's possible with same CommitteeIndex and different AttesatationData) for aggregate in aggregates: aggregate_and_proof = AggregateAndProof.create( aggregator_index=validator_index, aggregate=aggregate, selection_proof=selected_proofs[validator_index], ) self.import_attestation(aggregate_and_proof.aggregate, True) await self.p2p_node.broadcast_beacon_aggregate_and_proof(aggregate_and_proof) aggregate_and_proofs += (aggregate_and_proof,) return aggregate_and_proofs
def test_validate_proposer_signature( slots_per_epoch, shard_count, proposer_privkey, proposer_pubkey, is_valid_signature, sample_beacon_block_params, sample_beacon_state_params, beacon_chain_shard_number, target_committee_size, max_deposit_amount, config): state = BeaconState(**sample_beacon_state_params).copy( validator_registry=tuple( mock_validator_record(proposer_pubkey, config) for _ in range(10) ), validator_balances=(max_deposit_amount,) * 10, ) default_block = BeaconBlock(**sample_beacon_block_params) empty_signature_block_root = default_block.block_without_signature_root proposal_signed_root = Proposal( state.slot, beacon_chain_shard_number, empty_signature_block_root, ).signed_root proposed_block = BeaconBlock(**sample_beacon_block_params).copy( signature=bls.sign( message_hash=proposal_signed_root, privkey=proposer_privkey, domain=get_domain( Fork( config.GENESIS_FORK_VERSION.to_bytes(4, 'little'), config.GENESIS_FORK_VERSION.to_bytes(4, 'little'), config.GENESIS_EPOCH, ), slot_to_epoch(state.slot, slots_per_epoch), SignatureDomain.DOMAIN_BEACON_BLOCK, ), ), ) if is_valid_signature: validate_proposer_signature( state, proposed_block, beacon_chain_shard_number, CommitteeConfig(config), ) else: with pytest.raises(ValidationError): validate_proposer_signature( state, proposed_block, beacon_chain_shard_number, CommitteeConfig(config), )