diff --git a/tests/core/pyspec/eth2spec/test/helpers/proposer_slashings.py b/tests/core/pyspec/eth2spec/test/helpers/proposer_slashings.py index ac2ebcf9c..87b4f5ca0 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/proposer_slashings.py +++ b/tests/core/pyspec/eth2spec/test/helpers/proposer_slashings.py @@ -1,22 +1,55 @@ from eth2spec.test.helpers.block_header import sign_block_header from eth2spec.test.helpers.keys import pubkey_to_privkey +from eth2spec.test.helpers.state import get_balance -def get_valid_proposer_slashing(spec, state, signed_1=False, signed_2=False): - current_epoch = spec.get_current_epoch(state) - validator_index = spec.get_active_validator_indices(state, current_epoch)[-1] - privkey = pubkey_to_privkey[state.validators[validator_index].pubkey] +def check_proposer_slashing_effect(spec, pre_state, state, slashed_index): + slashed_validator = state.validators[slashed_index] + assert slashed_validator.slashed + assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH + assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH + + proposer_index = spec.get_beacon_proposer_index(state) + slash_penalty = state.validators[slashed_index].effective_balance // spec.MIN_SLASHING_PENALTY_QUOTIENT + whistleblower_reward = state.validators[slashed_index].effective_balance // spec.WHISTLEBLOWER_REWARD_QUOTIENT + if proposer_index != slashed_index: + # slashed validator lost initial slash penalty + assert ( + get_balance(state, slashed_index) + == get_balance(pre_state, slashed_index) - slash_penalty + ) + # block proposer gained whistleblower reward + # >= because proposer could have reported multiple + assert ( + get_balance(state, proposer_index) + >= get_balance(pre_state, proposer_index) + whistleblower_reward + ) + else: + # proposer reported themself so get penalty and reward + # >= because proposer could have reported multiple + assert ( + get_balance(state, slashed_index) + >= get_balance(pre_state, slashed_index) - slash_penalty + whistleblower_reward + ) + + +def get_valid_proposer_slashing(spec, state, random_root=b'\x99' * 32, + slashed_index=None, signed_1=False, signed_2=False): + if slashed_index is None: + current_epoch = spec.get_current_epoch(state) + slashed_index = spec.get_active_validator_indices(state, current_epoch)[-1] + privkey = pubkey_to_privkey[state.validators[slashed_index].pubkey] slot = state.slot header_1 = spec.BeaconBlockHeader( slot=slot, - proposer_index=validator_index, + proposer_index=slashed_index, parent_root=b'\x33' * 32, state_root=b'\x44' * 32, body_root=b'\x55' * 32, ) header_2 = header_1.copy() - header_2.parent_root = b'\x99' * 32 + header_2.parent_root = random_root if signed_1: signed_header_1 = sign_block_header(spec, state, header_1, privkey) diff --git a/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_proposer_slashing.py b/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_proposer_slashing.py index 7657518fc..e2a6a3fe0 100644 --- a/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_proposer_slashing.py +++ b/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_proposer_slashing.py @@ -1,8 +1,9 @@ from eth2spec.test.context import spec_state_test, expect_assertion_error, always_bls, with_all_phases +from eth2spec.test.helpers.block import build_empty_block_for_next_slot from eth2spec.test.helpers.block_header import sign_block_header from eth2spec.test.helpers.keys import privkeys -from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing -from eth2spec.test.helpers.state import get_balance, next_epoch +from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing, check_proposer_slashing_effect +from eth2spec.test.helpers.state import next_epoch def run_proposer_slashing_processing(spec, state, proposer_slashing, valid=True): @@ -14,6 +15,8 @@ def run_proposer_slashing_processing(spec, state, proposer_slashing, valid=True) If ``valid == False``, run expecting ``AssertionError`` """ + pre_state = state.copy() + yield 'pre', state yield 'proposer_slashing', proposer_slashing @@ -22,25 +25,31 @@ def run_proposer_slashing_processing(spec, state, proposer_slashing, valid=True) yield 'post', None return - proposer_index = proposer_slashing.signed_header_1.message.proposer_index - pre_proposer_balance = get_balance(state, proposer_index) - spec.process_proposer_slashing(state, proposer_slashing) yield 'post', state - # check if slashed - slashed_validator = state.validators[proposer_index] - 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(state, proposer_index) < pre_proposer_balance + slashed_proposer_index = proposer_slashing.signed_header_1.message.proposer_index + check_proposer_slashing_effect(spec, pre_state, state, slashed_proposer_index) @with_all_phases @spec_state_test def test_success(spec, state): + # Get proposer for next slot + block = build_empty_block_for_next_slot(spec, state) + proposer_index = block.proposer_index + + # Create slashing for same proposer + proposer_slashing = get_valid_proposer_slashing(spec, state, + slashed_index=proposer_index, + signed_1=True, signed_2=True) + + yield from run_proposer_slashing_processing(spec, state, proposer_slashing) + + +@with_all_phases +@spec_state_test +def test_success_slashed_and_proposer_index_the_same(spec, state): proposer_slashing = get_valid_proposer_slashing(spec, state, signed_1=True, signed_2=True) yield from run_proposer_slashing_processing(spec, state, proposer_slashing) diff --git a/tests/core/pyspec/eth2spec/test/phase_0/sanity/test_blocks.py b/tests/core/pyspec/eth2spec/test/phase_0/sanity/test_blocks.py index 29a9dcca2..eebfe5171 100644 --- a/tests/core/pyspec/eth2spec/test/phase_0/sanity/test_blocks.py +++ b/tests/core/pyspec/eth2spec/test/phase_0/sanity/test_blocks.py @@ -1,5 +1,3 @@ -from copy import deepcopy - from eth2spec.utils import bls from eth2spec.test.helpers.state import get_balance, state_transition_and_sign_block, next_slot, next_epoch @@ -7,7 +5,7 @@ from eth2spec.test.helpers.block import build_empty_block_for_next_slot, build_e transition_unsigned_block from eth2spec.test.helpers.keys import privkeys, pubkeys from eth2spec.test.helpers.attester_slashings import get_valid_attester_slashing, get_indexed_attestation_participants -from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing +from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing, check_proposer_slashing_effect from eth2spec.test.helpers.attestations import get_valid_attestation, fill_block_shard_transitions_by_attestations from eth2spec.test.helpers.deposits import prepare_state_and_deposit @@ -231,11 +229,11 @@ def test_empty_epoch_transition_not_finalizing(spec, state): @spec_state_test def test_proposer_slashing(spec, state): # copy for later balance lookups. - pre_state = deepcopy(state) + pre_state = state.copy() proposer_slashing = get_valid_proposer_slashing(spec, state, signed_1=True, signed_2=True) - validator_index = proposer_slashing.signed_header_1.message.proposer_index + slashed_index = proposer_slashing.signed_header_1.message.proposer_index - assert not state.validators[validator_index].slashed + assert not state.validators[slashed_index].slashed yield 'pre', state @@ -250,7 +248,85 @@ def test_proposer_slashing(spec, state): yield 'blocks', [signed_block] yield 'post', state - # check if slashed + check_proposer_slashing_effect(spec, pre_state, state, slashed_index) + + +@with_all_phases +@spec_state_test +def test_double_same_proposer_slashings_same_block(spec, state): + proposer_slashing = get_valid_proposer_slashing(spec, state, signed_1=True, signed_2=True) + slashed_index = proposer_slashing.signed_header_1.message.proposer_index + assert not state.validators[slashed_index].slashed + + yield 'pre', state + + block = build_empty_block_for_next_slot(spec, state) + block.body.proposer_slashings = [proposer_slashing, proposer_slashing] + signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=True) + + yield 'blocks', [signed_block] + yield 'post', None + + +@with_all_phases +@spec_state_test +def test_double_similar_proposer_slashings_same_block(spec, state): + slashed_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-1] + + # Same validator, but different slashable offences in the same block + proposer_slashing_1 = get_valid_proposer_slashing(spec, state, random_root=b'\xaa' * 32, + slashed_index=slashed_index, + signed_1=True, signed_2=True) + proposer_slashing_2 = get_valid_proposer_slashing(spec, state, random_root=b'\xbb' * 32, + slashed_index=slashed_index, + signed_1=True, signed_2=True) + assert not state.validators[slashed_index].slashed + + yield 'pre', state + + block = build_empty_block_for_next_slot(spec, state) + block.body.proposer_slashings = [proposer_slashing_1, proposer_slashing_2] + signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=True) + + yield 'blocks', [signed_block] + yield 'post', None + + +@with_all_phases +@spec_state_test +def test_multiple_different_proposer_slashings_same_block(spec, state): + pre_state = state.copy() + + num_slashings = 3 + proposer_slashings = [] + for i in range(num_slashings): + slashed_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[i] + assert not state.validators[slashed_index].slashed + + proposer_slashing = get_valid_proposer_slashing(spec, state, + slashed_index=slashed_index, + signed_1=True, signed_2=True) + proposer_slashings.append(proposer_slashing) + + yield 'pre', state + + # + # Add to state via block transition + # + block = build_empty_block_for_next_slot(spec, state) + block.body.proposer_slashings = proposer_slashings + + signed_block = state_transition_and_sign_block(spec, state, block) + + yield 'blocks', [signed_block] + yield 'post', state + + for proposer_slashing in proposer_slashings: + slashed_index = proposer_slashing.signed_header_1.message.proposer_index + check_proposer_slashing_effect(spec, pre_state, state, slashed_index) + + +def check_attester_slashing_effect(spec, pre_state, state, validator_index): slashed_validator = state.validators[validator_index] assert slashed_validator.slashed assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH @@ -258,12 +334,16 @@ def test_proposer_slashing(spec, state): # lost whistleblower reward assert get_balance(state, validator_index) < get_balance(pre_state, validator_index) + proposer_index = spec.get_beacon_proposer_index(state) + # gained whistleblower reward + assert get_balance(state, proposer_index) > get_balance(pre_state, proposer_index) + @with_all_phases @spec_state_test def test_attester_slashing(spec, state): # copy for later balance lookups. - pre_state = deepcopy(state) + pre_state = state.copy() attester_slashing = get_valid_attester_slashing(spec, state, signed_1=True, signed_2=True) validator_index = get_indexed_attestation_participants(spec, attester_slashing.attestation_1)[0] @@ -283,19 +363,11 @@ def test_attester_slashing(spec, state): yield 'blocks', [signed_block] yield 'post', state - slashed_validator = state.validators[validator_index] - 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(state, validator_index) < get_balance(pre_state, validator_index) + check_attester_slashing_effect(spec, pre_state, state, validator_index) - proposer_index = spec.get_beacon_proposer_index(state) - # gained whistleblower reward - assert ( - get_balance(state, proposer_index) > - get_balance(pre_state, proposer_index) - ) +# TODO: currently mainnet limits attester-slashings per block to 1. +# When this is increased, it should be tested to cover various combinations +# of duplicate slashings and overlaps of slashed attestations within the same block @with_all_phases @@ -448,35 +520,38 @@ def test_attestation(spec, state): assert spec.hash_tree_root(state.previous_epoch_attestations) == pre_current_attestations_root +def prepare_signed_exits(spec, state, indices): + domain = spec.get_domain(state, spec.DOMAIN_VOLUNTARY_EXIT) + + def create_signed_exit(index): + exit = spec.VoluntaryExit( + epoch=spec.get_current_epoch(state), + validator_index=index, + ) + signing_root = spec.compute_signing_root(exit, domain) + return spec.SignedVoluntaryExit(message=exit, signature=bls.Sign(privkeys[index], signing_root)) + + return [create_signed_exit(index) for index in indices] + + # In phase1 a committee is computed for PERSISTENT_COMMITTEE_PERIOD slots ago, # exceeding the minimal-config randao mixes memory size. +# Applies to all voluntary-exit sanity block tests. + @with_phases(['phase0']) @spec_state_test def test_voluntary_exit(spec, state): - validator_index = spec.get_active_validator_indices( - state, - spec.get_current_epoch(state) - )[-1] + validator_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-1] # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + signed_exits = prepare_signed_exits(spec, state, [validator_index]) yield 'pre', state - voluntary_exit = spec.VoluntaryExit( - epoch=spec.get_current_epoch(state), - validator_index=validator_index, - ) - domain = spec.get_domain(state, spec.DOMAIN_VOLUNTARY_EXIT) - signing_root = spec.compute_signing_root(voluntary_exit, domain) - signed_voluntary_exit = spec.SignedVoluntaryExit( - message=voluntary_exit, - signature=bls.Sign(privkeys[validator_index], signing_root) - ) - # Add to state via block transition initiate_exit_block = build_empty_block_for_next_slot(spec, state) - initiate_exit_block.body.voluntary_exits.append(signed_voluntary_exit) + initiate_exit_block.body.voluntary_exits = signed_exits signed_initiate_exit_block = state_transition_and_sign_block(spec, state, initiate_exit_block) assert state.validators[validator_index].exit_epoch < spec.FAR_FUTURE_EPOCH @@ -491,6 +566,59 @@ def test_voluntary_exit(spec, state): assert state.validators[validator_index].exit_epoch < spec.FAR_FUTURE_EPOCH +@with_phases(['phase0']) +@spec_state_test +def test_double_validator_exit_same_block(spec, state): + validator_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-1] + + # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit + state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + # Same index tries to exit twice, but should only be able to do so once. + signed_exits = prepare_signed_exits(spec, state, [validator_index, validator_index]) + yield 'pre', state + + # Add to state via block transition + initiate_exit_block = build_empty_block_for_next_slot(spec, state) + initiate_exit_block.body.voluntary_exits = signed_exits + signed_initiate_exit_block = state_transition_and_sign_block(spec, state, initiate_exit_block, expect_fail=True) + + yield 'blocks', [signed_initiate_exit_block] + yield 'post', None + + +@with_phases(['phase0']) +@spec_state_test +def test_multiple_different_validator_exits_same_block(spec, state): + validator_indices = [ + spec.get_active_validator_indices(state, spec.get_current_epoch(state))[i] + for i in range(3) + ] + # move state forward PERSISTENT_COMMITTEE_PERIOD epochs to allow for exit + state.slot += spec.PERSISTENT_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + signed_exits = prepare_signed_exits(spec, state, validator_indices) + yield 'pre', state + + # Add to state via block transition + initiate_exit_block = build_empty_block_for_next_slot(spec, state) + initiate_exit_block.body.voluntary_exits = signed_exits + signed_initiate_exit_block = state_transition_and_sign_block(spec, state, initiate_exit_block) + + for index in validator_indices: + assert state.validators[index].exit_epoch < spec.FAR_FUTURE_EPOCH + + # Process within epoch transition + exit_block = build_empty_block(spec, state, state.slot + spec.SLOTS_PER_EPOCH) + signed_exit_block = state_transition_and_sign_block(spec, state, exit_block) + + yield 'blocks', [signed_initiate_exit_block, signed_exit_block] + yield 'post', state + + for index in validator_indices: + assert state.validators[index].exit_epoch < spec.FAR_FUTURE_EPOCH + + @with_all_phases @spec_state_test def test_balance_driven_status_transitions(spec, state):