From c17a95a175d7ef149572e36b823eda28163f707b Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Wed, 14 Oct 2020 17:33:14 -0600 Subject: [PATCH 1/3] add note about how slashings and exits can interact. add test --- specs/phase0/validator.md | 4 ++ .../eth2spec/test/helpers/voluntary_exits.py | 15 ++++++ .../test/phase0/sanity/test_blocks.py | 17 +----- .../phase0/sanity/test_multi_operations.py | 53 +++++++++++++++++++ 4 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 tests/core/pyspec/eth2spec/test/phase0/sanity/test_multi_operations.py diff --git a/specs/phase0/validator.md b/specs/phase0/validator.md index be9a16755..82bd4cf6e 100644 --- a/specs/phase0/validator.md +++ b/specs/phase0/validator.md @@ -358,6 +358,10 @@ The `proof` for each deposit must be constructed against the deposit root contai Up to `MAX_VOLUNTARY_EXITS`, [`VoluntaryExit`](./beacon-chain.md#voluntaryexit) objects can be included in the `block`. The exits must satisfy the verification conditions found in [exits processing](./beacon-chain.md#voluntary-exits). +*Note*: If a slashing for a validator is included in the same block as a +voluntary exit, the voluntary exit will fail and cause the block to be invalid +due to the slashing being processed first. Implementers must take heed of this +operation interaction when packing blocks. #### Packaging into a `SignedBeaconBlock` diff --git a/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py b/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py index 55310ef7d..28232cc23 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py +++ b/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py @@ -1,4 +1,19 @@ from eth2spec.utils import bls +from eth2spec.test.helpers.keys import privkeys + + +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] def sign_voluntary_exit(spec, state, voluntary_exit, privkey): diff --git a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py index 721d68add..dc56965ac 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py +++ b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py @@ -9,7 +9,7 @@ from eth2spec.test.helpers.block import ( sign_block, transition_unsigned_block, ) -from eth2spec.test.helpers.keys import privkeys, pubkeys +from eth2spec.test.helpers.keys import pubkeys from eth2spec.test.helpers.attester_slashings import ( get_valid_attester_slashing_by_indices, get_valid_attester_slashing, @@ -18,6 +18,7 @@ from eth2spec.test.helpers.attester_slashings import ( from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing, check_proposer_slashing_effect from eth2spec.test.helpers.attestations import get_valid_attestation from eth2spec.test.helpers.deposits import prepare_state_and_deposit +from eth2spec.test.helpers.voluntary_exits import prepare_signed_exits from eth2spec.test.helpers.shard_transitions import get_shard_transition_of_committee from eth2spec.test.context import ( @@ -794,20 +795,6 @@ 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 SHARD_COMMITTEE_PERIOD slots ago, # exceeding the minimal-config randao mixes memory size. # Applies to all voluntary-exit sanity block tests. diff --git a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_multi_operations.py b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_multi_operations.py new file mode 100644 index 000000000..1e1ed42f5 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_multi_operations.py @@ -0,0 +1,53 @@ +from eth2spec.test.helpers.state import ( + state_transition_and_sign_block, +) +from eth2spec.test.helpers.block import ( + build_empty_block_for_next_slot, +) +from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing +from eth2spec.test.helpers.voluntary_exits import prepare_signed_exits + +from eth2spec.test.context import ( + spec_state_test, + with_all_phases, +) + + +def run_slash_and_exit(spec, state, slash_index, exit_index, valid=True): + # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit + state.slot += spec.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + yield 'pre', state + + block = build_empty_block_for_next_slot(spec, state) + + proposer_slashing = get_valid_proposer_slashing( + spec, state, slashed_index=slash_index, signed_1=True, signed_2=True) + signed_exit = prepare_signed_exits(spec, state, [exit_index])[0] + + block.body.proposer_slashings.append(proposer_slashing) + block.body.voluntary_exits.append(signed_exit) + + signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=(not valid)) + + yield 'blocks', [signed_block] + + if valid: + yield 'post', state + else: + yield 'post', None + + +@with_all_phases +@spec_state_test +def test_slash_and_exit_same_index(spec, state): + validator_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-1] + run_slash_and_exit(spec, state, validator_index, validator_index, valid=False) + + +@with_all_phases +@spec_state_test +def test_slash_and_exit_diff_index(spec, state): + slash_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-1] + exit_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-2] + run_slash_and_exit(spec, state, slash_index, exit_index) From aab58e47008e161336dd591bd52e3c39c046a288 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Wed, 14 Oct 2020 17:36:10 -0600 Subject: [PATCH 2/3] update sanity generators to generate the oepration block tests as well --- .../eth2spec/test/phase0/sanity/test_multi_operations.py | 4 ++-- tests/generators/sanity/main.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_multi_operations.py b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_multi_operations.py index 1e1ed42f5..3c14a7d9d 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_multi_operations.py +++ b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_multi_operations.py @@ -42,7 +42,7 @@ def run_slash_and_exit(spec, state, slash_index, exit_index, valid=True): @spec_state_test def test_slash_and_exit_same_index(spec, state): validator_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-1] - run_slash_and_exit(spec, state, validator_index, validator_index, valid=False) + yield from run_slash_and_exit(spec, state, validator_index, validator_index, valid=False) @with_all_phases @@ -50,4 +50,4 @@ def test_slash_and_exit_same_index(spec, state): def test_slash_and_exit_diff_index(spec, state): slash_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-1] exit_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-2] - run_slash_and_exit(spec, state, slash_index, exit_index) + yield from run_slash_and_exit(spec, state, slash_index, exit_index) diff --git a/tests/generators/sanity/main.py b/tests/generators/sanity/main.py index 83166f0cf..afd255dcb 100644 --- a/tests/generators/sanity/main.py +++ b/tests/generators/sanity/main.py @@ -34,6 +34,7 @@ def create_provider(fork_name: str, handler_name: str, if __name__ == "__main__": phase_0_mods = {key: 'eth2spec.test.phase0.sanity.test_' + key for key in [ 'blocks', + 'multi_operations', 'slots', ]} phase_1_mods = {**{key: 'eth2spec.test.phase1.sanity.test_' + key for key in [ From a34970f8a3a1ee32e06b845bf3de6d70e69090d0 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Fri, 16 Oct 2020 08:27:28 -0600 Subject: [PATCH 3/3] move op tests to test_blocks --- .../test/phase0/sanity/test_blocks.py | 45 ++++++++++++++++ .../phase0/sanity/test_multi_operations.py | 53 ------------------- tests/generators/sanity/main.py | 1 - 3 files changed, 45 insertions(+), 54 deletions(-) delete mode 100644 tests/core/pyspec/eth2spec/test/phase0/sanity/test_multi_operations.py diff --git a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py index dc56965ac..44aabdb76 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py +++ b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_blocks.py @@ -882,6 +882,51 @@ def test_multiple_different_validator_exits_same_block(spec, state): assert state.validators[index].exit_epoch < spec.FAR_FUTURE_EPOCH +def run_slash_and_exit(spec, state, slash_index, exit_index, valid=True): + """ + Helper function to run a test that slashes and exits two validators + """ + # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit + state.slot += spec.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH + + yield 'pre', state + + block = build_empty_block_for_next_slot(spec, state) + + proposer_slashing = get_valid_proposer_slashing( + spec, state, slashed_index=slash_index, signed_1=True, signed_2=True) + signed_exit = prepare_signed_exits(spec, state, [exit_index])[0] + + block.body.proposer_slashings.append(proposer_slashing) + block.body.voluntary_exits.append(signed_exit) + + signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=(not valid)) + + yield 'blocks', [signed_block] + + if valid: + yield 'post', state + else: + yield 'post', None + + +@with_all_phases +@spec_state_test +@disable_process_reveal_deadlines +def test_slash_and_exit_same_index(spec, state): + validator_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-1] + yield from run_slash_and_exit(spec, state, validator_index, validator_index, valid=False) + + +@with_all_phases +@spec_state_test +@disable_process_reveal_deadlines +def test_slash_and_exit_diff_index(spec, state): + slash_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-1] + exit_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-2] + yield from run_slash_and_exit(spec, state, slash_index, exit_index) + + @with_all_phases @spec_state_test def test_balance_driven_status_transitions(spec, state): diff --git a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_multi_operations.py b/tests/core/pyspec/eth2spec/test/phase0/sanity/test_multi_operations.py deleted file mode 100644 index 3c14a7d9d..000000000 --- a/tests/core/pyspec/eth2spec/test/phase0/sanity/test_multi_operations.py +++ /dev/null @@ -1,53 +0,0 @@ -from eth2spec.test.helpers.state import ( - state_transition_and_sign_block, -) -from eth2spec.test.helpers.block import ( - build_empty_block_for_next_slot, -) -from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing -from eth2spec.test.helpers.voluntary_exits import prepare_signed_exits - -from eth2spec.test.context import ( - spec_state_test, - with_all_phases, -) - - -def run_slash_and_exit(spec, state, slash_index, exit_index, valid=True): - # move state forward SHARD_COMMITTEE_PERIOD epochs to allow for exit - state.slot += spec.SHARD_COMMITTEE_PERIOD * spec.SLOTS_PER_EPOCH - - yield 'pre', state - - block = build_empty_block_for_next_slot(spec, state) - - proposer_slashing = get_valid_proposer_slashing( - spec, state, slashed_index=slash_index, signed_1=True, signed_2=True) - signed_exit = prepare_signed_exits(spec, state, [exit_index])[0] - - block.body.proposer_slashings.append(proposer_slashing) - block.body.voluntary_exits.append(signed_exit) - - signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=(not valid)) - - yield 'blocks', [signed_block] - - if valid: - yield 'post', state - else: - yield 'post', None - - -@with_all_phases -@spec_state_test -def test_slash_and_exit_same_index(spec, state): - validator_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-1] - yield from run_slash_and_exit(spec, state, validator_index, validator_index, valid=False) - - -@with_all_phases -@spec_state_test -def test_slash_and_exit_diff_index(spec, state): - slash_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-1] - exit_index = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[-2] - yield from run_slash_and_exit(spec, state, slash_index, exit_index) diff --git a/tests/generators/sanity/main.py b/tests/generators/sanity/main.py index afd255dcb..83166f0cf 100644 --- a/tests/generators/sanity/main.py +++ b/tests/generators/sanity/main.py @@ -34,7 +34,6 @@ def create_provider(fork_name: str, handler_name: str, if __name__ == "__main__": phase_0_mods = {key: 'eth2spec.test.phase0.sanity.test_' + key for key in [ 'blocks', - 'multi_operations', 'slots', ]} phase_1_mods = {**{key: 'eth2spec.test.phase1.sanity.test_' + key for key in [