diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index 9c26203a5f..ea8edc15b5 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -59,6 +59,7 @@ - [`get_current_epoch`](#get_current_epoch) - [`get_epoch_start_slot`](#get_epoch_start_slot) - [`is_active_validator`](#is_active_validator) + - [`is_slashable_validator`](#is_slashable_validator) - [`get_active_validator_indices`](#get_active_validator_indices) - [`get_balance`](#get_balance) - [`set_balance`](#set_balance) @@ -744,6 +745,18 @@ def is_active_validator(validator: Validator, epoch: Epoch) -> bool: return validator.activation_epoch <= epoch < validator.exit_epoch ``` +### `is_slashable_validator` +```python +def is_slashable_validator(validator: Validator, epoch: Epoch) -> bool: + """ + Check if ``validator`` is slashable. + """ + return ( + validator.activation_epoch <= epoch < validator.withdrawable_epoch and + validator.slashed is False + ) +``` + ### `get_active_validator_indices` ```python @@ -1427,7 +1440,6 @@ def slash_validator(state: BeaconState, index: ValidatorIndex) -> None: Note that this function mutates ``state``. """ validator = state.validator_registry[index] - assert state.slot < get_epoch_start_slot(validator.withdrawable_epoch) # [TO BE REMOVED IN PHASE 2] exit_validator(state, index) state.latest_slashed_balances[get_current_epoch(state) % LATEST_SLASHED_EXIT_LENGTH] += get_effective_balance(state, index) @@ -2356,8 +2368,8 @@ def process_proposer_slashing(state: BeaconState, assert slot_to_epoch(proposer_slashing.header_1.slot) == slot_to_epoch(proposer_slashing.header_2.slot) # But the headers are different assert proposer_slashing.header_1 != proposer_slashing.header_2 - # Proposer is not yet slashed - assert proposer.slashed is False + # Check proposer is slashable + assert is_slashable_validator(proposer, get_current_epoch(state)) # Signatures are valid for header in (proposer_slashing.header_1, proposer_slashing.header_2): assert bls_verify( @@ -2396,7 +2408,7 @@ def process_attester_slashing(state: BeaconState, index for index in attestation1.validator_indices if ( index in attestation2.validator_indices and - state.validator_registry[index].slashed is False + is_slashable_validator(state.validator_registry[index], get_current_epoch(state)) ) ] assert len(slashable_indices) >= 1 diff --git a/tests/phase0/block_processing/test_process_proposer_slashing.py b/tests/phase0/block_processing/test_process_proposer_slashing.py new file mode 100644 index 0000000000..467d2164ba --- /dev/null +++ b/tests/phase0/block_processing/test_process_proposer_slashing.py @@ -0,0 +1,97 @@ +from copy import deepcopy +import pytest + +import build.phase0.spec as spec +from build.phase0.spec import ( + get_balance, + get_current_epoch, + process_proposer_slashing, +) +from tests.phase0.helpers import ( + get_valid_proposer_slashing, +) + +# mark entire file as 'header' +pytestmark = pytest.mark.proposer_slashings + + +def run_proposer_slashing_processing(state, proposer_slashing, valid=True): + """ + Run ``process_proposer_slashing`` returning the pre and post state. + If ``valid == False``, run expecting ``AssertionError`` + """ + post_state = deepcopy(state) + + if not valid: + with pytest.raises(AssertionError): + process_proposer_slashing(post_state, proposer_slashing) + return state, None + + process_proposer_slashing(post_state, proposer_slashing) + + slashed_validator = post_state.validator_registry[proposer_slashing.proposer_index] + assert not slashed_validator.initiated_exit + assert slashed_validator.slashed + assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH + assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH + # lost whistleblower reward + assert ( + get_balance(post_state, proposer_slashing.proposer_index) < + get_balance(state, proposer_slashing.proposer_index) + ) + + return state, post_state + + +def test_success(state): + proposer_slashing = get_valid_proposer_slashing(state) + + pre_state, post_state = run_proposer_slashing_processing(state, proposer_slashing) + + return pre_state, proposer_slashing, post_state + + +def test_epochs_are_different(state): + proposer_slashing = get_valid_proposer_slashing(state) + + # set slots to be in different epochs + proposer_slashing.header_2.slot += spec.SLOTS_PER_EPOCH + + pre_state, post_state = run_proposer_slashing_processing(state, proposer_slashing, False) + + return pre_state, proposer_slashing, post_state + + +def test_headers_are_same(state): + proposer_slashing = get_valid_proposer_slashing(state) + + # set headers to be the same + proposer_slashing.header_2 = proposer_slashing.header_1 + + pre_state, post_state = run_proposer_slashing_processing(state, proposer_slashing, False) + + return pre_state, proposer_slashing, post_state + + +def test_proposer_is_slashed(state): + proposer_slashing = get_valid_proposer_slashing(state) + + # set proposer to slashed + state.validator_registry[proposer_slashing.proposer_index].slashed = True + + pre_state, post_state = run_proposer_slashing_processing(state, proposer_slashing, False) + + return pre_state, proposer_slashing, post_state + + +def test_proposer_is_withdrawn(state): + proposer_slashing = get_valid_proposer_slashing(state) + + # set proposer withdrawable_epoch in past + current_epoch = get_current_epoch(state) + proposer_index = proposer_slashing.proposer_index + state.validator_registry[proposer_index].withdrawable_epoch = current_epoch - 1 + + pre_state, post_state = run_proposer_slashing_processing(state, proposer_slashing, False) + + return pre_state, proposer_slashing, post_state diff --git a/tests/phase0/helpers.py b/tests/phase0/helpers.py index 5c61685a62..3987289bff 100644 --- a/tests/phase0/helpers.py +++ b/tests/phase0/helpers.py @@ -7,14 +7,18 @@ from build.phase0.spec import ( # constants EMPTY_SIGNATURE, + ZERO_HASH, # SSZ AttestationData, + BeaconBlockHeader, Deposit, DepositInput, DepositData, Eth1Data, + ProposerSlashing, VoluntaryExit, # functions + get_active_validator_indices, get_block_root, get_current_epoch, get_domain, @@ -199,3 +203,43 @@ def build_deposit(state, ) return deposit, root, deposit_data_leaves + + +def get_valid_proposer_slashing(state): + current_epoch = get_current_epoch(state) + validator_index = get_active_validator_indices(state.validator_registry, current_epoch)[-1] + privkey = pubkey_to_privkey[state.validator_registry[validator_index].pubkey] + slot = state.slot + + header_1 = BeaconBlockHeader( + slot=slot, + previous_block_root=ZERO_HASH, + state_root=ZERO_HASH, + block_body_root=ZERO_HASH, + signature=EMPTY_SIGNATURE, + ) + header_2 = deepcopy(header_1) + header_2.previous_block_root = b'\x02' * 32 + header_2.slot = slot + 1 + + domain = get_domain( + fork=state.fork, + epoch=get_current_epoch(state), + domain_type=spec.DOMAIN_BEACON_BLOCK, + ) + header_1.signature = bls.sign( + message_hash=signed_root(header_1), + privkey=privkey, + domain=domain, + ) + header_2.signature = bls.sign( + message_hash=signed_root(header_2), + privkey=privkey, + domain=domain, + ) + + return ProposerSlashing( + proposer_index=validator_index, + header_1=header_1, + header_2=header_2, + ) diff --git a/tests/phase0/test_sanity.py b/tests/phase0/test_sanity.py index ec03fb3555..444075a133 100644 --- a/tests/phase0/test_sanity.py +++ b/tests/phase0/test_sanity.py @@ -46,6 +46,7 @@ build_deposit_data, build_empty_block_for_next_slot, force_registry_change_at_next_epoch, + get_valid_proposer_slashing, ) @@ -117,42 +118,8 @@ def test_empty_epoch_transition_not_finalizing(state): def test_proposer_slashing(state, pubkeys, privkeys): test_state = deepcopy(state) - current_epoch = get_current_epoch(test_state) - validator_index = get_active_validator_indices(test_state.validator_registry, current_epoch)[-1] - privkey = privkeys[validator_index] - slot = spec.GENESIS_SLOT - header_1 = BeaconBlockHeader( - slot=slot, - previous_block_root=ZERO_HASH, - state_root=ZERO_HASH, - block_body_root=ZERO_HASH, - signature=EMPTY_SIGNATURE, - ) - header_2 = deepcopy(header_1) - header_2.previous_block_root = b'\x02' * 32 - header_2.slot = slot + 1 - - domain = get_domain( - fork=test_state.fork, - epoch=get_current_epoch(test_state), - domain_type=spec.DOMAIN_BEACON_BLOCK, - ) - header_1.signature = bls.sign( - message_hash=signed_root(header_1), - privkey=privkey, - domain=domain, - ) - header_2.signature = bls.sign( - message_hash=signed_root(header_2), - privkey=privkey, - domain=domain, - ) - - proposer_slashing = ProposerSlashing( - proposer_index=validator_index, - header_1=header_1, - header_2=header_2, - ) + proposer_slashing = get_valid_proposer_slashing(state) + validator_index = proposer_slashing.proposer_index # # Add to state via block transition