diff --git a/configs/mainnet.yaml b/configs/mainnet.yaml index 60bd1c087..42845c235 100644 --- a/configs/mainnet.yaml +++ b/configs/mainnet.yaml @@ -132,8 +132,8 @@ MIN_SLASHING_PENALTY_QUOTIENT: 32 # --------------------------------------------------------------- # 2**4 (= 16) MAX_PROPOSER_SLASHINGS: 16 -# 2**0 (= 1) -MAX_ATTESTER_SLASHINGS: 1 +# 2**1 (= 2) +MAX_ATTESTER_SLASHINGS: 2 # 2**7 (= 128) MAX_ATTESTATIONS: 128 # 2**4 (= 16) diff --git a/configs/minimal.yaml b/configs/minimal.yaml index 5c1511e6d..d8e346ffa 100644 --- a/configs/minimal.yaml +++ b/configs/minimal.yaml @@ -132,8 +132,8 @@ MIN_SLASHING_PENALTY_QUOTIENT: 32 # --------------------------------------------------------------- # 2**4 (= 16) MAX_PROPOSER_SLASHINGS: 16 -# 2**0 (= 1) -MAX_ATTESTER_SLASHINGS: 1 +# 2**1 (= 2) +MAX_ATTESTER_SLASHINGS: 2 # 2**7 (= 128) MAX_ATTESTATIONS: 128 # 2**4 (= 16) diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index 899778fd9..6d60d76e3 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -252,7 +252,7 @@ The following values are (non-configurable) constants used throughout the specif | Name | Value | | - | - | | `MAX_PROPOSER_SLASHINGS` | `2**4` (= 16) | -| `MAX_ATTESTER_SLASHINGS` | `2**0` (= 1) | +| `MAX_ATTESTER_SLASHINGS` | `2**1` (= 2) | | `MAX_ATTESTATIONS` | `2**7` (= 128) | | `MAX_DEPOSITS` | `2**4` (= 16) | | `MAX_VOLUNTARY_EXITS` | `2**4` (= 16) | diff --git a/tests/core/pyspec/eth2spec/test/helpers/attester_slashings.py b/tests/core/pyspec/eth2spec/test/helpers/attester_slashings.py index 975f34c20..e743ca8ff 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/attester_slashings.py +++ b/tests/core/pyspec/eth2spec/test/helpers/attester_slashings.py @@ -1,5 +1,5 @@ from eth2spec.test.context import PHASE1 -from eth2spec.test.helpers.attestations import get_valid_attestation, sign_attestation +from eth2spec.test.helpers.attestations import get_valid_attestation, sign_attestation, sign_indexed_attestation def get_valid_attester_slashing(spec, state, signed_1=False, signed_2=False): @@ -17,6 +17,26 @@ def get_valid_attester_slashing(spec, state, signed_1=False, signed_2=False): ) +def get_valid_attester_slashing_by_indices(spec, state, indices_1, indices_2=None, signed_1=False, signed_2=False): + if indices_2 is None: + indices_2 = indices_1 + + assert indices_1 == sorted(indices_1) + assert indices_2 == sorted(indices_2) + + attester_slashing = get_valid_attester_slashing(spec, state) + + attester_slashing.attestation_1.attesting_indices = indices_1 + attester_slashing.attestation_2.attesting_indices = indices_2 + + if signed_1: + sign_indexed_attestation(spec, state, attester_slashing.attestation_1) + if signed_2: + sign_indexed_attestation(spec, state, attester_slashing.attestation_2) + + return attester_slashing + + def get_indexed_attestation_participants(spec, indexed_att): """ Wrapper around index-attestation to return the list of participant indices, regardless of spec phase. 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 1864006bd..1c0fa5eb9 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 @@ -4,7 +4,11 @@ from eth2spec.test.helpers.state import get_balance, state_transition_and_sign_b from eth2spec.test.helpers.block import build_empty_block_for_next_slot, build_empty_block, sign_block, \ 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.attester_slashings import ( + get_valid_attester_slashing_by_indices, + get_valid_attester_slashing, + get_indexed_attestation_participants, +) 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 @@ -326,13 +330,14 @@ def test_multiple_different_proposer_slashings_same_block(spec, state): 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 - assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH - # lost whistleblower reward - assert get_balance(state, validator_index) < get_balance(pre_state, validator_index) +def check_attester_slashing_effect(spec, pre_state, state, slashed_indices): + for slashed_index in slashed_indices: + 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 + # lost whistleblower reward + assert get_balance(state, slashed_index) < get_balance(pre_state, slashed_index) proposer_index = spec.get_beacon_proposer_index(state) # gained whistleblower reward @@ -346,9 +351,9 @@ def test_attester_slashing(spec, 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] + slashed_indices = get_indexed_attestation_participants(spec, attester_slashing.attestation_1) - assert not state.validators[validator_index].slashed + assert not any(state.validators[i].slashed for i in slashed_indices) yield 'pre', state @@ -363,13 +368,124 @@ def test_attester_slashing(spec, state): yield 'blocks', [signed_block] yield 'post', state - check_attester_slashing_effect(spec, pre_state, state, validator_index) + check_attester_slashing_effect(spec, pre_state, state, slashed_indices) + + +@with_all_phases +@spec_state_test +def test_duplicate_attester_slashing(spec, state): + # Skip test if config cannot handle multiple AttesterSlashings per block + if spec.MAX_ATTESTER_SLASHINGS < 2: + return + + attester_slashing = get_valid_attester_slashing(spec, state, signed_1=True, signed_2=True) + attester_slashings = [attester_slashing, attester_slashing.copy()] + slashed_indices = get_indexed_attestation_participants(spec, attester_slashing.attestation_1) + + assert not any(state.validators[i].slashed for i in slashed_indices) + + yield 'pre', state + + # + # Add to state via block transition + # + block = build_empty_block_for_next_slot(spec, state) + block.body.attester_slashings = attester_slashings + + signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=True) + + yield 'blocks', [signed_block] + yield 'post', None + + +# All AttesterSlashing tests should be adopted for Phase 1 but helper support is not yet there + +@with_phases(['phase0']) +@spec_state_test +def test_multiple_attester_slashings_no_overlap(spec, state): + # Skip test if config cannot handle multiple AttesterSlashings per block + if spec.MAX_ATTESTER_SLASHINGS < 2: + return + + # copy for later balance lookups. + pre_state = state.copy() + + full_indices = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[:8] + half_length = len(full_indices) // 2 + + attester_slashing_1 = get_valid_attester_slashing_by_indices( + spec, state, + full_indices[:half_length], signed_1=True, signed_2=True, + ) + attester_slashing_2 = get_valid_attester_slashing_by_indices( + spec, state, + full_indices[half_length:], signed_1=True, signed_2=True, + ) + attester_slashings = [attester_slashing_1, attester_slashing_2] + + assert not any(state.validators[i].slashed for i in full_indices) + + yield 'pre', state + + # + # Add to state via block transition + # + block = build_empty_block_for_next_slot(spec, state) + block.body.attester_slashings = attester_slashings + + signed_block = state_transition_and_sign_block(spec, state, block) + + yield 'blocks', [signed_block] + yield 'post', state + + check_attester_slashing_effect(spec, pre_state, state, full_indices) + + +@with_phases(['phase0']) +@spec_state_test +def test_multiple_attester_slashings_partial_overlap(spec, state): + # Skip test if config cannot handle multiple AttesterSlashings per block + if spec.MAX_ATTESTER_SLASHINGS < 2: + return + + # copy for later balance lookups. + pre_state = state.copy() + + full_indices = spec.get_active_validator_indices(state, spec.get_current_epoch(state))[:8] + one_third_length = len(full_indices) // 3 + + attester_slashing_1 = get_valid_attester_slashing_by_indices( + spec, state, + full_indices[:one_third_length * 2], signed_1=True, signed_2=True, + ) + attester_slashing_2 = get_valid_attester_slashing_by_indices( + spec, state, + full_indices[one_third_length:], signed_1=True, signed_2=True, + ) + attester_slashings = [attester_slashing_1, attester_slashing_2] + + assert not any(state.validators[i].slashed for i in full_indices) + + yield 'pre', state + + # + # Add to state via block transition + # + block = build_empty_block_for_next_slot(spec, state) + block.body.attester_slashings = attester_slashings + + signed_block = state_transition_and_sign_block(spec, state, block) + + yield 'blocks', [signed_block] + yield 'post', state + + check_attester_slashing_effect(spec, pre_state, state, full_indices) + # 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 @spec_state_test def test_proposer_after_inactive_index(spec, state):