def test_get_committee_assignment( genesis_state, slots_per_epoch, max_committees_per_slot, config, validator_count, state_epoch, epoch, ): state_slot = compute_start_slot_at_epoch(state_epoch, slots_per_epoch) state = genesis_state.set("slot", state_slot) committee_validator_count = [0 for _ in range(max_committees_per_slot)] slots = [] epoch_start_slot = compute_start_slot_at_epoch(epoch, slots_per_epoch) for validator_index in range(validator_count): assignment = get_committee_assignment(state, config, epoch, validator_index) assert assignment.slot >= epoch_start_slot assert assignment.slot < epoch_start_slot + slots_per_epoch committee_validator_count[assignment.committee_index] += 1 slots.append(assignment.slot) assert sum(committee_validator_count) == validator_count
def test_get_attesting_indices(genesis_state, config): state = genesis_state.set( "slot", compute_start_slot_at_epoch(3, config.SLOTS_PER_EPOCH)) target_epoch = state.current_epoch(config.SLOTS_PER_EPOCH) target_slot = compute_start_slot_at_epoch(target_epoch, config.SLOTS_PER_EPOCH) committee_index = 0 some_committee = get_beacon_committee(state, target_slot, committee_index, config) data = AttestationData.create( slot=target_slot, index=committee_index, target=Checkpoint.create(epoch=target_epoch), ) some_subset_count = random.randrange(1, len(some_committee) // 2) some_subset = random.sample(some_committee, some_subset_count) bitfield = get_empty_bitfield(len(some_committee)) for i, index in enumerate(some_committee): if index in some_subset: bitfield = set_voted(bitfield, i) indices = get_attesting_indices(state, data, bitfield, config) assert set(indices) == set(some_subset) assert len(indices) == len(some_subset)
def test_validate_attestation_data( genesis_state, sample_attestation_data_params, attestation_source_epoch, attestation_target_epoch, current_epoch, previous_justified_epoch, current_justified_epoch, slots_per_epoch, config, is_valid, ): state = genesis_state.copy( slot=compute_start_slot_at_epoch(current_epoch, slots_per_epoch) + 5, previous_justified_checkpoint=Checkpoint( epoch=previous_justified_epoch), current_justified_checkpoint=Checkpoint(epoch=current_justified_epoch), ) target_slot = compute_start_slot_at_epoch(current_epoch, config.SLOTS_PER_EPOCH) committee_index = 0 attestation_data = AttestationData(**sample_attestation_data_params).copy( slot=target_slot, index=committee_index, source=Checkpoint(epoch=attestation_source_epoch), target=Checkpoint(epoch=attestation_target_epoch), ) if is_valid: _validate_attestation_data(state, attestation_data, config) else: with pytest.raises(ValidationError): _validate_attestation_data(state, attestation_data, config)
def test_randao_reveal_validation( is_valid, epoch, expected_epoch, proposer_key_index, expected_proposer_key_index, privkeys, pubkeys, sample_fork_params, genesis_state, config, ): state = genesis_state.set( "slot", compute_start_slot_at_epoch(epoch, config.SLOTS_PER_EPOCH)) slots_per_epoch = config.SLOTS_PER_EPOCH domain = get_domain(state, SignatureDomain.DOMAIN_RANDAO, slots_per_epoch) signing_root = compute_signing_root(SerializableUint64(epoch), domain) proposer_privkey = privkeys[proposer_key_index] randao_reveal = bls.sign(proposer_privkey, signing_root) try: validate_randao_reveal( state=state, proposer_index=expected_proposer_key_index, epoch=expected_epoch, randao_reveal=randao_reveal, slots_per_epoch=slots_per_epoch, ) except ValidationError: if is_valid: raise else: if not is_valid: pytest.fail("Did not raise")
async def validate_peer_status(chain: BaseBeaconChain, peer_status: Status) -> None: state_machine = chain.get_state_machine() state = chain.get_head_state() config = state_machine.config if peer_status.head_fork_version != state.fork.current_version: raise IrrelevantNetwork( "`fork_version` mismatches: " f"peer_status.head_fork_version={peer_status.head_fork_version}, " f"state.fork.current_version={state.fork.current_version}") # Can not validate the checkpoint with `finalized_epoch` higher than ours if peer_status.finalized_epoch > state.finalized_checkpoint.epoch: return # Edge case where nothing is finalized yet if (peer_status.finalized_epoch == 0 and peer_status.finalized_root == ZERO_ROOT): return finalized_epoch_start_slot = compute_start_slot_at_epoch( peer_status.finalized_epoch, config.SLOTS_PER_EPOCH, ) finalized_root = chain.get_canonical_block_root(finalized_epoch_start_slot) if peer_status.finalized_root != finalized_root: raise IrrelevantNetwork( "`finalized_root` mismatches: " f"peer_status.finalized_root={peer_status.finalized_root.hex()}, " f"peer_status.finalized_epoch={peer_status.finalized_epoch}, " f"our `finalized_root` at the same `finalized_epoch`={finalized_root.hex()}" )
def test_get_matching_target_attestations(genesis_state, config): some_epoch = GENESIS_EPOCH + 20 some_slot = compute_start_slot_at_epoch(some_epoch, config.SLOTS_PER_EPOCH) some_target_root = b"\x33" * 32 target_attestations = tuple( PendingAttestation.create( data=AttestationData.create(target=Checkpoint.create(root=some_target_root)) ) for _ in range(3) ) current_epoch_attestations = target_attestations + tuple( PendingAttestation.create( data=AttestationData.create(target=Checkpoint.create(root=b"\x44" * 32)) ) for _ in range(3) ) state = genesis_state.transform( ["slot"], some_slot + 1, ["block_roots", some_slot % config.SLOTS_PER_HISTORICAL_ROOT], some_target_root, ["current_epoch_attestations"], current_epoch_attestations, ) attestations = get_matching_target_attestations(state, some_epoch, config) assert attestations == target_attestations
def create_mock_attester_slashing_is_surround_vote( state: BeaconState, config: Eth2Config, keymap: Dict[BLSPubkey, int], attestation_epoch: Epoch, ) -> AttesterSlashing: # target_epoch_2 < target_epoch_1 attestation_slot_2 = compute_start_slot_at_epoch(attestation_epoch, config.SLOTS_PER_EPOCH) attestation_slot_1 = Slot(attestation_slot_2 + config.SLOTS_PER_EPOCH) slashable_attestation_1 = create_mock_slashable_attestation( state.mset("slot", attestation_slot_1, "current_justified_epoch", config.GENESIS_EPOCH), config, keymap, attestation_slot_1, ) slashable_attestation_2 = create_mock_slashable_attestation( state.mset( "slot", attestation_slot_1, "current_justified_epoch", config.GENESIS_EPOCH + 1, # source_epoch_1 < source_epoch_2 ), config, keymap, attestation_slot_2, ) return AttesterSlashing.create(attestation_1=slashable_attestation_1, attestation_2=slashable_attestation_2)
def test_process_slashings( genesis_state, config, current_epoch, slashings, slots_per_epoch, epochs_per_slashings_vector, expected_penalty, ): state = genesis_state.mset( "slot", compute_start_slot_at_epoch(current_epoch, slots_per_epoch), "slashings", slashings, ) slashing_validator_index = 0 validator = state.validators[slashing_validator_index].mset( "slashed", True, "withdrawable_epoch", current_epoch + epochs_per_slashings_vector // 2, ) state = state.transform(["validators", slashing_validator_index], validator) result_state = process_slashings(state, config) penalty = (state.balances[slashing_validator_index] - result_state.balances[slashing_validator_index]) assert penalty == expected_penalty
def test_validate_eligible_exit_epoch( genesis_state, keymap, current_epoch, voluntary_exit_epoch, slots_per_epoch, config, success, ): state = genesis_state.set( "slot", compute_start_slot_at_epoch(current_epoch, slots_per_epoch)) validator_index = 0 signed_voluntary_exit = create_mock_voluntary_exit( state, config, keymap, validator_index, exit_epoch=voluntary_exit_epoch) voluntary_exit = signed_voluntary_exit.message if success: _validate_eligible_exit_epoch(voluntary_exit.epoch, state.current_epoch(slots_per_epoch)) else: with pytest.raises(ValidationError): _validate_eligible_exit_epoch(voluntary_exit.epoch, state.current_epoch(slots_per_epoch))
def test_process_slashings( genesis_state, config, current_epoch, slashings, slots_per_epoch, epochs_per_slashings_vector, expected_penalty, ): state = genesis_state.copy( slot=compute_start_slot_at_epoch(current_epoch, slots_per_epoch), slashings=slashings, ) slashing_validator_index = 0 validator = state.validators[slashing_validator_index].copy( slashed=True, withdrawable_epoch=current_epoch + epochs_per_slashings_vector // 2, ) state = state.update_validator(slashing_validator_index, validator) result_state = process_slashings(state, config) penalty = ( state.balances[slashing_validator_index] - result_state.balances[slashing_validator_index] ) assert penalty == expected_penalty
def _should_update_justified_checkpoint( self, new_justified_checkpoint: Checkpoint) -> bool: """ To address the bouncing attack, only update conflicting justified checkpoints in the fork choice if in the early slots of the epoch. Otherwise, delay incorporation of new justified checkpoint until next epoch boundary. See https://ethresear.ch/t/prevention-of-bouncing-attack-on-ffg/6114 for more detailed analysis and discussion. """ current_slot = self.get_current_slot() slots_since_epoch_start = compute_slots_since_epoch_start( current_slot, self._config.SLOTS_PER_EPOCH) within_safe_slots = (slots_since_epoch_start < self._config.SAFE_SLOTS_TO_UPDATE_JUSTIFIED) if within_safe_slots: return True new_justified_block = self._context.blocks[ new_justified_checkpoint.root] justified_epoch = self._context.justified_checkpoint.epoch if new_justified_block.slot <= compute_start_slot_at_epoch( justified_epoch, self._config.SLOTS_PER_EPOCH): return False justified_root = self._context.justified_checkpoint.root justified_ancestor = self.get_ancestor_root( new_justified_checkpoint.root, self._context.justified_slot) return justified_ancestor == justified_root
def on_block( self, block: BaseBeaconBlock, post_state: BeaconState = None, state_machine: BaseBeaconStateMachine = None, ) -> None: """ Handler to update the fork choice context upon receiving a new ``block``. This handler requests the ``post_state`` of this block to avoid recomputing it if it is already known. """ # NOTE: this invariant should hold based on how we handle # block importing in the chain but we will sanity check for now assert block.parent_root in self._context.block_states pre_state = self._context.block_states[block.parent_root] # NOTE: this invariant should hold based on how we handle # block importing in the chain but we will sanity check for now assert (self._context.time >= pre_state.genesis_time + block.slot * self._config.SECONDS_PER_SLOT) root = block.signing_root self._context.blocks[root] = block finalized_slot = self._context.finalized_slot finalized_ancestor = self.get_ancestor_root(root, finalized_slot) is_ancestor_of_finalized_block = ( finalized_ancestor == self._context.finalized_checkpoint.root) if not is_ancestor_of_finalized_block: raise ValidationError( f"block with signing root {root.hex()} is not a descendant of the finalized" f" checkpoint with root {finalized_ancestor.hex()}") # NOTE: sanity check implied by the previous verification on finalized ancestor assert block.slot > compute_start_slot_at_epoch( self._context.finalized_checkpoint.epoch, self._config.SLOTS_PER_EPOCH) if not post_state: # NOTE: error to not provide a post_state and not provide a way to compute it assert state_machine is not None post_state, _ = state_machine.import_block(block, pre_state) self._context.block_states[root] = post_state if (post_state.current_justified_checkpoint.epoch > self._context.justified_checkpoint.epoch): self._context.best_justified_checkpoint = ( post_state.current_justified_checkpoint) if self._should_update_justified_checkpoint( post_state.current_justified_checkpoint): self._context.justified_checkpoint = ( post_state.current_justified_checkpoint) if (post_state.finalized_checkpoint.epoch > self._context.finalized_checkpoint.epoch): self._context.finalized_checkpoint = post_state.finalized_checkpoint
def test_get_matching_head_attestations(genesis_state, config): some_epoch = config.GENESIS_EPOCH + 20 some_slot = ( compute_start_slot_at_epoch(some_epoch, config.SLOTS_PER_EPOCH) + config.SLOTS_PER_EPOCH // 4) some_target_root = b"\x33" * 32 target_attestations = tuple( (PendingAttestation.create(data=AttestationData.create( slot=some_slot - 1, index=0, beacon_block_root=some_target_root, target=Checkpoint.create(epoch=some_epoch - 1), )) for i in range(3))) current_epoch_attestations = target_attestations + tuple( (PendingAttestation.create(data=AttestationData.create( beacon_block_root=b"\x44" * 32, target=Checkpoint.create(epoch=some_epoch - 1), )) for _ in range(3))) state = genesis_state.mset( "slot", some_slot, "block_roots", tuple(some_target_root for _ in range(config.SLOTS_PER_HISTORICAL_ROOT)), "current_epoch_attestations", current_epoch_attestations, ) attestations = get_matching_head_attestations(state, some_epoch, config) assert attestations == target_attestations
def test_validate_validator_minimum_lifespan( genesis_state, keymap, current_epoch, activation_epoch, slots_per_epoch, persistent_committee_period, success, ): state = genesis_state.copy( slot=compute_start_slot_at_epoch(current_epoch, slots_per_epoch)) validator_index = 0 validator = state.validators[validator_index].copy( activation_epoch=activation_epoch) state = state.update_validator(validator_index, validator) if success: _validate_validator_minimum_lifespan( validator, state.current_epoch(slots_per_epoch), persistent_committee_period) else: with pytest.raises(ValidationError): _validate_validator_minimum_lifespan( validator, state.current_epoch(slots_per_epoch), persistent_committee_period, )
def test_get_matching_source_attestations(genesis_state, current_epoch, target_epoch, success, config): state = genesis_state.mset( "slot", compute_start_slot_at_epoch(current_epoch, config.SLOTS_PER_EPOCH), "current_epoch_attestations", (PendingAttestation.create(data=AttestationData.create( beacon_block_root=current_epoch.to_bytes(32, "little"))), ), "previous_epoch_attestations", (PendingAttestation.create(data=AttestationData.create( beacon_block_root=(current_epoch - 1).to_bytes(32, "little"))), ), ) if success: attestations = get_matching_source_attestations( state, target_epoch, config) else: with pytest.raises(InvalidEpochError): get_matching_source_attestations(state, target_epoch, config) return if current_epoch == target_epoch: assert attestations == state.current_epoch_attestations else: assert attestations == state.previous_epoch_attestations
def compute_slots_since_epoch_start(slot: Slot, slots_per_epoch: int) -> Slot: return Slot( slot - compute_start_slot_at_epoch( compute_epoch_at_slot(slot, slots_per_epoch), slots_per_epoch ) )
def test_validate_validator_minimum_lifespan( genesis_state, keymap, current_epoch, activation_epoch, slots_per_epoch, persistent_committee_period, success, ): state = genesis_state.set( "slot", compute_start_slot_at_epoch(current_epoch, slots_per_epoch)) validator_index = 0 validator = state.validators[validator_index].set("activation_epoch", activation_epoch) state = state.transform(["validators", validator_index], validator) if success: _validate_validator_minimum_lifespan( validator, state.current_epoch(slots_per_epoch), persistent_committee_period) else: with pytest.raises(ValidationError): _validate_validator_minimum_lifespan( validator, state.current_epoch(slots_per_epoch), persistent_committee_period, )
def test_validate_voluntary_exit(genesis_state, keymap, slots_per_epoch, persistent_committee_period, config): state = genesis_state.copy(slot=compute_start_slot_at_epoch( config.GENESIS_EPOCH + persistent_committee_period, slots_per_epoch)) validator_index = 0 valid_voluntary_exit = create_mock_voluntary_exit(state, config, keymap, validator_index) validate_voluntary_exit(state, valid_voluntary_exit, slots_per_epoch, persistent_committee_period)
def _get_target_checkpoint(state: BeaconState, head_root: Root, config: Eth2Config) -> Checkpoint: epoch = state.current_epoch(config.SLOTS_PER_EPOCH) start_slot = compute_start_slot_at_epoch(epoch, config.SLOTS_PER_EPOCH) if start_slot == state.slot: root = head_root else: root = get_block_root_at_slot(state, start_slot, config.SLOTS_PER_HISTORICAL_ROOT) return Checkpoint.create(epoch=epoch, root=root)
def _get_target_root(state: BeaconState, config: Eth2Config, beacon_block_root: Root) -> Root: epoch = compute_epoch_at_slot(state.slot, config.SLOTS_PER_EPOCH) epoch_start_slot = compute_start_slot_at_epoch(epoch, config.SLOTS_PER_EPOCH) if epoch_start_slot == state.slot: return beacon_block_root else: return get_block_root(state, epoch, config.SLOTS_PER_EPOCH, config.SLOTS_PER_HISTORICAL_ROOT)
def validate_start_slot(chain: BaseBeaconChain, start_slot: Slot) -> None: config = chain.get_state_machine().config state = chain.get_head_state() finalized_epoch_start_slot = compute_start_slot_at_epoch( epoch=state.finalized_checkpoint.epoch, slots_per_epoch=config.SLOTS_PER_EPOCH, ) if start_slot < finalized_epoch_start_slot: raise ValidationError( f"`start_slot`({start_slot}) lower than our" f" latest finalized slot({finalized_epoch_start_slot})")
def test_validate_voluntary_exit(genesis_state, keymap, slots_per_epoch, shard_committee_period, config): state = genesis_state.set( "slot", compute_start_slot_at_epoch(GENESIS_EPOCH + shard_committee_period, slots_per_epoch), ) validator_index = 0 valid_voluntary_exit = create_mock_voluntary_exit(state, config, keymap, validator_index) validate_voluntary_exit(state, valid_voluntary_exit, slots_per_epoch, shard_committee_period)
def test_process_voluntary_exits( genesis_state, sample_beacon_block_params, sample_beacon_block_body_params, config, keymap, success, ): state = genesis_state.copy( slot=compute_start_slot_at_epoch( config.GENESIS_EPOCH + config.PERSISTENT_COMMITTEE_PERIOD, config.SLOTS_PER_EPOCH, ) ) validator_index = 0 validator = state.validators[validator_index].copy( activation_epoch=config.GENESIS_EPOCH ) state = state.update_validator(validator_index, validator) valid_voluntary_exit = create_mock_voluntary_exit( state, config, keymap, validator_index ) if success: block_body = BeaconBlockBody(**sample_beacon_block_body_params).copy( voluntary_exits=(valid_voluntary_exit,) ) block = SerenityBeaconBlock(**sample_beacon_block_params).copy( slot=state.slot, body=block_body ) new_state = process_voluntary_exits(state, block, config) updated_validator = new_state.validators[validator_index] assert updated_validator.exit_epoch != FAR_FUTURE_EPOCH assert updated_validator.exit_epoch > state.current_epoch( config.SLOTS_PER_EPOCH ) assert updated_validator.withdrawable_epoch == ( updated_validator.exit_epoch + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY ) else: invalid_voluntary_exit = valid_voluntary_exit.copy( signature=b"\x12" * 96 # Put wrong signature ) block_body = BeaconBlockBody(**sample_beacon_block_body_params).copy( voluntary_exits=(invalid_voluntary_exit,) ) block = SerenityBeaconBlock(**sample_beacon_block_params).copy( slot=state.slot, body=block_body ) with pytest.raises(ValidationError): process_voluntary_exits(state, block, config)
def _determine_sync_requests(self, peer_id: PeerID, status: Status) -> Iterable[SyncRequest]: """ If the peer has a higher finalized epoch or head slot, sync blocks from them. """ head_state = self._chain.get_canonical_head_state() finalized_slot = compute_start_slot_at_epoch( head_state.finalized_checkpoint.epoch, self._eth2_config.SLOTS_PER_EPOCH) while finalized_slot < status.head_slot: count = min(status.head_slot - finalized_slot, MAX_REQUEST_BLOCKS) yield SyncRequest(peer_id, Slot(finalized_slot + 1), count) finalized_slot += count
def test_validate_attestation_data( genesis_state, sample_attestation_data_params, attestation_source_epoch, attestation_target_epoch, current_epoch, previous_justified_epoch, current_justified_epoch, slots_per_epoch, config, is_valid, ): state = genesis_state.mset( "slot", compute_start_slot_at_epoch(current_epoch, slots_per_epoch) + 5, "previous_justified_checkpoint", Checkpoint.create(epoch=previous_justified_epoch), "current_justified_checkpoint", Checkpoint.create(epoch=current_justified_epoch), ) target_slot = compute_start_slot_at_epoch(current_epoch, config.SLOTS_PER_EPOCH) committee_index = 0 attestation_data = AttestationData.create(**sample_attestation_data_params).mset( "slot", target_slot, "index", committee_index, "source", Checkpoint.create(epoch=attestation_source_epoch), "target", Checkpoint.create(epoch=attestation_target_epoch), ) if is_valid: _validate_attestation_data(state, attestation_data, config) else: with pytest.raises(ValidationError): _validate_attestation_data(state, attestation_data, config)
def test_get_unslashed_attesting_indices(genesis_state, config): state = genesis_state.set( "slot", compute_start_slot_at_epoch(3, config.SLOTS_PER_EPOCH) ) target_epoch = state.current_epoch(config.SLOTS_PER_EPOCH) target_slot = compute_start_slot_at_epoch(target_epoch, config.SLOTS_PER_EPOCH) committee_index = 0 some_committee = get_beacon_committee( state, target_slot, committee_index, CommitteeConfig(config) ) data = AttestationData.create( slot=state.slot, index=committee_index, target=Checkpoint.create(epoch=target_epoch), ) some_subset_count = random.randrange(1, len(some_committee) // 2) some_subset = random.sample(some_committee, some_subset_count) bitfield = get_empty_bitfield(len(some_committee)) for i, index in enumerate(some_committee): if index in some_subset: if random.choice([True, False]): state = state.transform(["validators", index, "slashed"], True) bitfield = set_voted(bitfield, i) some_subset = tuple( filter(lambda index: not state.validators[index].slashed, some_subset) ) indices = get_unslashed_attesting_indices( state, (PendingAttestation.create(data=data, aggregation_bits=bitfield),), CommitteeConfig(config), ) assert set(indices) == set(some_subset) assert len(indices) == len(some_subset)
def _determine_sync_request(self, peer_id: PeerID, status: Status) -> Optional[SyncRequest]: """ If the peer has a higher finalized epoch or head slot, sync blocks from them. """ head_state = self._chain.get_canonical_head_state() finalized_slot = compute_start_slot_at_epoch( head_state.finalized_checkpoint.epoch, self._eth2_config.SLOTS_PER_EPOCH) if finalized_slot < status.head_slot: span = status.head_slot - finalized_slot return SyncRequest(peer_id, Slot(finalized_slot + 1), span) else: return None
def _get_slot_with_validator_selected(candidate_indices, state, config): epoch = state.current_epoch(config.SLOTS_PER_EPOCH) epoch_start_slot = compute_start_slot_at_epoch(epoch, config.SLOTS_PER_EPOCH) for index in candidate_indices: try: for slot in range(epoch_start_slot, epoch_start_slot + config.SLOTS_PER_EPOCH): state = state.copy(slot=slot) if is_proposer(state, index, config): return slot, index except NoCommitteeAssignment: continue raise Exception( "Check the parameters of the genesis state; the above code should return" " some proposer if the set of ``candidate_indices`` is big enough." )
def create_mock_attester_slashing_is_double_vote( state: BeaconState, config: Eth2Config, keymap: Dict[BLSPubkey, int], attestation_epoch: Epoch, ) -> AttesterSlashing: attestation_slot_1 = compute_start_slot_at_epoch(attestation_epoch, config.SLOTS_PER_EPOCH) attestation_slot_2 = Slot(attestation_slot_1 + 1) slashable_attestation_1 = create_mock_slashable_attestation( state, config, keymap, attestation_slot_1) slashable_attestation_2 = create_mock_slashable_attestation( state, config, keymap, attestation_slot_2) return AttesterSlashing.create(attestation_1=slashable_attestation_1, attestation_2=slashable_attestation_2)
def _get_finalized_root_by_epoch(self, epoch: Epoch) -> Optional[Root]: slots_per_epoch = self._eth2_config.SLOTS_PER_EPOCH head_state = self._chain.get_head_state() finalized_checkpoint = head_state.finalized_checkpoint if epoch > finalized_checkpoint.epoch: return None if epoch == finalized_checkpoint.epoch: return finalized_checkpoint.root # NOTE: get a historical finalized root # This root will be the block at the start slot of the ``epoch`` # in our canonical chain as implied by having a more recent # finalized head. slot = compute_start_slot_at_epoch(epoch, slots_per_epoch) return self._chain.get_canonical_block_root(slot)