Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Not allow slashed validator to become a proposer #3175

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions specs/deneb/beacon-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
- [`kzg_commitment_to_versioned_hash`](#kzg_commitment_to_versioned_hash)
- [`tx_peek_blob_versioned_hashes`](#tx_peek_blob_versioned_hashes)
- [`verify_kzg_commitments_against_transactions`](#verify_kzg_commitments_against_transactions)
- [Modified `compute_proposer_index`](#modified-compute_proposer_index)
- [Modified `get_beacon_proposer_index`](#modified-get_beacon_proposer_index)
- [Beacon chain state transition function](#beacon-chain-state-transition-function)
- [Block processing](#block-processing)
- [Execution payload](#execution-payload)
Expand Down Expand Up @@ -191,6 +193,49 @@ def verify_kzg_commitments_against_transactions(transactions: Sequence[Transacti
return all_versioned_hashes == [kzg_commitment_to_versioned_hash(commitment) for commitment in kzg_commitments]
```

#### Modified `compute_proposer_index`

*Note:* Disallowing slashed validator to become a proposer is the only modification of this function.

```python
def compute_proposer_index(state: BeaconState, indices: Sequence[ValidatorIndex], seed: Bytes32) -> ValidatorIndex:
"""
Return from ``indices`` a random index sampled by effective balance.
"""
assert len(indices) > 0
MAX_RANDOM_BYTE = 2**8 - 1
i = uint64(0)
total = uint64(len(indices))
while True:
candidate_index = indices[compute_shuffled_index(i % total, total, seed)]
random_byte = hash(seed + uint_to_bytes(uint64(i // 32)))[i % 32]
effective_balance = state.validators[candidate_index].effective_balance
# [Modified in Deneb]
slashed = state.validators[candidate_index].slashed
if effective_balance * MAX_RANDOM_BYTE >= MAX_EFFECTIVE_BALANCE * random_byte and not slashed:
return candidate_index
i += 1
```

#### Modified `get_beacon_proposer_index`

*Note:* Modified to read proposer index from the state to ensure the output stays unaffected if proposing validator gets slashed.

```python
def get_beacon_proposer_index(state: BeaconState) -> ValidatorIndex:
"""
Return the beacon proposer index at the current slot.
"""
# [New in Deneb]
if state.latest_block_header.slot == state.slot:
return state.latest_block_header.proposer_index

epoch = get_current_epoch(state)
seed = hash(get_seed(state, epoch, DOMAIN_BEACON_PROPOSER) + uint_to_bytes(state.slot))
indices = get_active_validator_indices(state, epoch)
return compute_proposer_index(state, indices, seed)
```

## Beacon chain state transition function

### Block processing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
compute_committee_indices,
run_sync_committee_processing,
run_successful_sync_committee_test,
_build_block_for_next_slot_with_sync_participation,
validate_sync_committee_rewards,
)
from eth2spec.test.helpers.voluntary_exits import (
get_unslashed_exited_validators,
Expand Down Expand Up @@ -702,3 +704,37 @@ def test_sync_committee_with_nonparticipating_withdrawable_member(spec, state):
)
)
yield from run_sync_committee_processing(spec, state, block)


@with_altair_and_later
@spec_state_test
@always_bls
def test_slashed_proposer_rewarded_for_sync_aggregate_inclusion(spec, state):
committee_indices = compute_committee_indices(state)
committee_bits = [True for _ in committee_indices]
block = _build_block_for_next_slot_with_sync_participation(spec, state, committee_indices, committee_bits)

pre_state = state.copy()

# process block header
spec.process_slots(state, block.slot)
spec.process_block_header(state, block)

# slash proposer
state.validators[block.proposer_index].slashed = True

yield 'pre', state
yield 'sync_aggregate', block.body.sync_aggregate

spec.process_sync_aggregate(state, block.body.sync_aggregate)

yield 'post', state

validate_sync_committee_rewards(
spec,
pre_state,
state,
committee_indices,
committee_bits,
block.proposer_index
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
always_bls,
spec_state_test,
with_deneb_and_later,
expect_assertion_error
expect_assertion_error,
)
from eth2spec.test.helpers.execution_payload import (
compute_el_block_hash,
Expand Down Expand Up @@ -156,3 +156,13 @@ def test_blob_sidecar_signature_incorrect(spec, state):
signed_blob_sidecar = spec.SignedBlobSidecar(message=blob_sidecars[1], signature=sidecar_signature)

assert not spec.verify_blob_sidecar_signature(state, signed_blob_sidecar)


@with_deneb_and_later
@spec_state_test
def test_slashed_validator_not_elected_for_proposal(spec, state):
spec.process_slots(state, state.slot + 1)
proposer_index = spec.get_beacon_proposer_index(state)
state.validators[proposer_index].slashed = True

assert spec.get_beacon_proposer_index(state) != proposer_index
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
low_balances,
with_custom_state,
single_phase,
with_altair_and_later,
)
from eth2spec.test.helpers.attestations import (
run_attestation_processing,
Expand All @@ -19,6 +20,7 @@
transition_to_slot_via_block,
)
from eth2spec.utils.ssz.ssz_typing import Bitlist
from eth2spec.test.helpers.block import build_empty_block


@with_all_phases
Expand Down Expand Up @@ -564,3 +566,23 @@ def test_invalid_incorrect_target_included_after_epoch_delay(spec, state):
sign_attestation(spec, state, attestation)

yield from run_attestation_processing(spec, state, attestation, valid=False)


@with_altair_and_later
@spec_state_test
def test_slashed_proposer_rewarded_for_attestation_inclusion(spec, state):
attestation = get_valid_attestation(spec, state, signed=True)
next_slots(spec, state, spec.MIN_ATTESTATION_INCLUSION_DELAY)

# Process block header as proposer index is read from the state since Deneb
block = build_empty_block(spec, state)
spec.process_block_header(state, block)

# Slash proposer
state.validators[block.proposer_index].slashed = True
pre_state_proposer_balance = state.balances[block.proposer_index]

yield from run_attestation_processing(spec, state, attestation)

# Check proposer gets rewarded
assert state.balances[block.proposer_index] > pre_state_proposer_balance
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
get_balance,
next_epoch_via_block,
)
from eth2spec.test.helpers.block import build_empty_block_for_next_slot
from eth2spec.test.helpers.forks import is_post_deneb


def run_attester_slashing_processing(spec, state, attester_slashing, valid=True):
Expand All @@ -26,6 +28,12 @@ def run_attester_slashing_processing(spec, state, attester_slashing, valid=True)
If ``valid == False``, run expecting ``AssertionError``
"""

# Makes `get_beacon_proposer_index` respond properly
if is_post_deneb(spec):
block = build_empty_block_for_next_slot(spec, state)
spec.process_slots(state, state.slot + 1)
spec.process_block_header(state, block)

yield 'pre', state
yield 'attester_slashing', attester_slashing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,10 @@ def test_invalid_proposer_slashed(spec, state):
next_slot(spec, stub_state)
proposer_index = spec.get_beacon_proposer_index(stub_state)

# build a block
block = build_empty_block_for_next_slot(spec, state)

# set proposer to slashed
state.validators[proposer_index].slashed = True

block = build_empty_block_for_next_slot(spec, state)

yield from run_block_header_processing(spec, state, block, valid=False)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from eth2spec.test.helpers.keys import privkeys
from eth2spec.test.helpers.proposer_slashings import get_valid_proposer_slashing, check_proposer_slashing_effect
from eth2spec.test.helpers.state import next_epoch
from eth2spec.test.helpers.forks import is_post_deneb


def run_proposer_slashing_processing(spec, state, proposer_slashing, valid=True):
Expand All @@ -17,6 +18,12 @@ def run_proposer_slashing_processing(spec, state, proposer_slashing, valid=True)

pre_state = state.copy()

# Makes `get_beacon_proposer_index` respond properly
if is_post_deneb(spec):
block = build_empty_block_for_next_slot(spec, state)
spec.process_slots(state, state.slot + 1)
spec.process_block_header(state, block)

yield 'pre', state
yield 'proposer_slashing', proposer_slashing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
spec_state_test,
always_bls, with_phases, with_all_phases,
)
from eth2spec.test.helpers.constants import PHASE0

from eth2spec.test.helpers.constants import PHASE0, ALTAIR, BELLATRIX, CAPELLA
from eth2spec.test.helpers.attestations import build_attestation_data, get_valid_attestation
from eth2spec.test.helpers.block import build_empty_block
from eth2spec.test.helpers.deposits import prepare_state_and_deposit
Expand Down Expand Up @@ -476,3 +477,12 @@ def test_get_aggregate_and_proof_signature(spec, state):
privkey=privkey,
pubkey=pubkey,
)


@with_phases([PHASE0, ALTAIR, BELLATRIX, CAPELLA])
@spec_state_test
def test_slashed_validator_elected_for_proposal(spec, state):
proposer_index = spec.get_beacon_proposer_index(state)
state.validators[proposer_index].slashed = True

assert spec.get_beacon_proposer_index(state) == proposer_index