Skip to content
Merged
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
26 changes: 26 additions & 0 deletions specs/gloas/beacon-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
- [Modified `compute_proposer_indices`](#modified-compute_proposer_indices)
- [New `compute_ptc`](#new-compute_ptc)
- [Beacon state accessors](#beacon-state-accessors)
- [Modified `get_beacon_proposer_indices`](#modified-get_beacon_proposer_indices)
- [Modified `get_next_sync_committee_indices`](#modified-get_next_sync_committee_indices)
- [Modified `get_attestation_participation_flag_indices`](#modified-get_attestation_participation_flag_indices)
- [New `get_ptc`](#new-get_ptc)
Expand Down Expand Up @@ -118,6 +119,8 @@ Gloas is a consensus-layer upgrade containing a number of features. Including:
- [EIP-7732](https://eips.ethereum.org/EIPS/eip-7732): Enshrined
Proposer-Builder Separation
- [EIP-7843](https://eips.ethereum.org/EIPS/eip-7843): SLOTNUM opcode
- [EIP-8045](https://eips.ethereum.org/EIPS/eip-8045): Exclude slashed
validators from proposing
- [EIP-8061](https://eips.ethereum.org/EIPS/eip-8061): Increase exit and
consolidation churn

Expand Down Expand Up @@ -682,6 +685,29 @@ def compute_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SI

### Beacon state accessors

#### Modified `get_beacon_proposer_indices`

*Note*: `get_beacon_proposer_indices` is modified to exclude slashed validators
from the candidate pool before invoking `compute_proposer_indices`, so the newly
computed proposer indices only contain active and unslashed validators.

```python
def get_beacon_proposer_indices(
state: BeaconState, epoch: Epoch
) -> Vector[ValidatorIndex, SLOTS_PER_EPOCH]:
"""
Return the proposer indices for the given ``epoch``.
"""
# [Modified in Gloas:EIP8045]
indices = [
index
for index in get_active_validator_indices(state, epoch)
if not state.validators[index].slashed
]
seed = get_seed(state, epoch, DOMAIN_BEACON_PROPOSER)
return compute_proposer_indices(state, epoch, seed, indices)
```

#### Modified `get_next_sync_committee_indices`

*Note*: `get_next_sync_committee_indices` is modified to use
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from eth_consensus_specs.test.context import spec_state_test, with_gloas_and_later
from eth_consensus_specs.test.helpers.epoch_processing import run_epoch_processing_with
from eth_consensus_specs.test.helpers.state import next_epoch


@with_gloas_and_later
@spec_state_test
def test_proposer_lookahead_does_not_contain_slashed_validators(spec, state):
"""
[EIP-8045] The newly appended epoch of ``proposer_lookahead`` must not
reference slashed validators.
"""
for _ in range(spec.MIN_SEED_LOOKAHEAD + 1):
next_epoch(spec, state)

active = spec.get_active_validator_indices(state, spec.get_current_epoch(state))
to_slash = set(active[: len(active) // 2])
for validator_index in to_slash:
state.validators[validator_index].slashed = True

yield from run_epoch_processing_with(spec, state, "process_proposer_lookahead")

last_epoch_start = len(state.proposer_lookahead) - spec.SLOTS_PER_EPOCH
for validator_index in state.proposer_lookahead[last_epoch_start:]:
assert validator_index not in to_slash
assert not state.validators[validator_index].slashed


@with_gloas_and_later
@spec_state_test
def test_proposer_lookahead_full_with_many_slashed_validators(spec, state):
"""
[EIP-8045] The lookahead must still fill its full ``SLOTS_PER_EPOCH`` entries
when only a small subset of active validators remain unslashed.
"""
for _ in range(spec.MIN_SEED_LOOKAHEAD + 1):
next_epoch(spec, state)

active = spec.get_active_validator_indices(state, spec.get_current_epoch(state))
keep = max(spec.SLOTS_PER_EPOCH, 8)
assert len(active) > keep
for validator_index in active[keep:]:
state.validators[validator_index].slashed = True

yield from run_epoch_processing_with(spec, state, "process_proposer_lookahead")

last_epoch_start = len(state.proposer_lookahead) - spec.SLOTS_PER_EPOCH
last_epoch_proposers = list(state.proposer_lookahead[last_epoch_start:])
assert len(last_epoch_proposers) == spec.SLOTS_PER_EPOCH
for validator_index in last_epoch_proposers:
assert not state.validators[validator_index].slashed
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from eth_consensus_specs.test.context import (
spec_test,
with_phases,
with_state,
)
from eth_consensus_specs.test.helpers.constants import (
FULU,
GLOAS,
)
from eth_consensus_specs.test.helpers.fork_transition import do_fork_generate
from eth_consensus_specs.test.helpers.gloas.fork import GLOAS_FORK_TEST_META_TAGS
from eth_consensus_specs.test.helpers.state import (
next_epoch,
transition_to,
)
from eth_consensus_specs.test.utils import with_meta_tags


def _transition_to_last_slot_before_fork(spec, state):
"""
Advance a couple of epochs so the proposer lookahead is fully populated by
regular epoch processing, then stop at the last slot of the epoch right
before the fork (where ``do_fork_generate`` expects to take over).
"""
for _ in range(2):
next_epoch(spec, state)
fork_epoch = spec.get_current_epoch(state) + 1
transition_to(spec, state, fork_epoch * spec.SLOTS_PER_EPOCH - 1)
return fork_epoch


@with_phases(phases=[FULU], other_phases=[GLOAS])
@spec_test
@with_state
@with_meta_tags(GLOAS_FORK_TEST_META_TAGS)
def test_fork_proposer_lookahead_slashed_validators_carried_over(spec, phases, state):
"""
Test that both proposer lookahead epochs can contain slashed validators
after the fork.
"""
gloas = phases[GLOAS]
fork_epoch = _transition_to_last_slot_before_fork(spec, state)

# Slash half of the validator set
for validator_index in range(len(state.validators) // 2):
state.validators[validator_index].slashed = True

state, _ = yield from do_fork_generate(state, spec, gloas, fork_epoch, with_block=False)

# Both halves were computed under Fulu, so both still have slashed validators
half = spec.SLOTS_PER_EPOCH
assert any(state.validators[v].slashed for v in state.proposer_lookahead[:half])
assert any(state.validators[v].slashed for v in state.proposer_lookahead[half:])
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
from eth_consensus_specs.test.context import always_bls, spec_state_test, with_gloas_and_later
from eth_consensus_specs.test.context import (
always_bls,
GLOAS,
spec_state_test,
with_gloas_and_later,
with_phases,
)
from eth_consensus_specs.test.gloas.block_processing.test_process_payload_attestation import (
prepare_signed_payload_attestation,
)
Expand All @@ -11,6 +17,7 @@
)
from eth_consensus_specs.test.helpers.keys import builder_privkeys, privkeys
from eth_consensus_specs.test.helpers.state import (
next_epoch,
next_epoch_with_full_participation,
state_transition_and_sign_block,
)
Expand Down Expand Up @@ -590,3 +597,46 @@ def test_voluntary_exit_fails_after_parent_payload_withdrawal_request(spec, stat
signed_block = state_transition_and_sign_block(spec, state, block, expect_fail=True)
yield "blocks", [signed_block]
yield "post", None


@with_phases([GLOAS])
@spec_state_test
def test_proposer_lookahead_excludes_slashed_validators(spec, state):
"""
Test that slashed validators are excluded from the proposer lookahead.
"""
for _ in range(2):
next_epoch(spec, state)

current_epoch = spec.get_current_epoch(state)
new_half_epoch = current_epoch + spec.MIN_SEED_LOOKAHEAD + 1

# Slash some validators
for validator_index in range(len(state.validators) // 2):
state.validators[validator_index].slashed = True

# Fulu includes slashed validators
proposers_in_fulu = spec.fulu.get_beacon_proposer_indices(state, new_half_epoch)
assert any(state.validators[v].slashed for v in proposers_in_fulu)

# Gloas excludes slashed validators
proposers_in_gloas = spec.get_beacon_proposer_indices(state, new_half_epoch)
assert not any(state.validators[v].slashed for v in proposers_in_gloas)

# Cross into the next epoch with a block, picking a slot whose proposer is
# not one of the validators we slashed so the block is valid
epoch_n1_proposers = list(state.proposer_lookahead[spec.SLOTS_PER_EPOCH :])
offset = next(i for i, p in enumerate(epoch_n1_proposers) if not state.validators[p].slashed)
block_slot = (current_epoch + 1) * spec.SLOTS_PER_EPOCH + offset

yield "pre", state

block = build_empty_block(spec, state, slot=block_slot)
signed_block = state_transition_and_sign_block(spec, state, block)

yield "blocks", [signed_block]
yield "post", state

# The newly appended lookahead epoch matches the Gloas selection
last_epoch_start = len(state.proposer_lookahead) - spec.SLOTS_PER_EPOCH
assert list(state.proposer_lookahead[last_epoch_start:]) == list(proposers_in_gloas)