Skip to content

EIP-7917: Deterministic proposer lookahead #4190

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

Open
wants to merge 25 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e73b5d8
EIP-XXXX Stabilize next epoch proposer lookahead
linoscope Mar 23, 2025
fb3cd63
EIP-XXXX add simple happy case tests
linoscope Mar 23, 2025
3fbe39c
fixup: Code clean up
linoscope Mar 24, 2025
0a37fe7
fixup: Target correct fork
linoscope Mar 24, 2025
a4dd9eb
Merge branch 'dev' into stabalize-next-epoch-lookahead
jtraglia Mar 24, 2025
9b82cb4
Clean up comment
linoscope Mar 25, 2025
7485399
refactor based on review comments
linoscope Mar 25, 2025
6a1d923
fix fork check in genesis helper
linoscope Mar 25, 2025
8fb1a58
improve comments
linoscope Mar 25, 2025
208f368
fix off-by-one error
linoscope Mar 25, 2025
af1892b
fix bug in seed calculation
linoscope Mar 26, 2025
53df021
add proper EIP number (EIP7917)
linoscope Mar 26, 2025
94d9dde
fix lint errors
linoscope Mar 27, 2025
83daa1e
add test to check lookahead consistency before/after Fulu fork
linoscope Mar 29, 2025
b1b0144
add tests for lookahead changes due to EB changes
linoscope Apr 1, 2025
9610b02
fix test folder
linoscope Apr 4, 2025
898432d
fix import
linoscope Apr 4, 2025
0414e6c
fix imports
linoscope Apr 7, 2025
78b3810
add generator EIP-7917 tests
linoscope Apr 7, 2025
291bed7
Merge branch 'dev' into stabalize-next-epoch-lookahead
jtraglia Apr 7, 2025
a54040e
Run make lint
jtraglia Apr 7, 2025
cede1ae
better yielding of vectors
linoscope Apr 8, 2025
3c9cf75
move helper and fix type
linoscope Apr 17, 2025
ec0243d
Merge branch 'dev' into stabalize-next-epoch-lookahead
jtraglia Apr 21, 2025
835df3d
Run make lint
jtraglia Apr 21, 2025
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
145 changes: 145 additions & 0 deletions specs/fulu/beacon-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@
- [Block processing](#block-processing)
- [Execution payload](#execution-payload)
- [Modified `process_execution_payload`](#modified-process_execution_payload)
- [Containers](#containers)
- [Extended Containers](#extended-containers)
- [`BeaconState`](#beaconstate)
- [Helper functions](#helper-functions)
- [Misc](#misc)
- [New `compute_proposer_indices`](#new-compute_proposer_indices)
- [Beacon state accessors](#beacon-state-accessors)
- [Modified `get_beacon_proposer_index`](#modified-get_beacon_proposer_index)
- [Epoch processing](#epoch-processing)
- [Modified `process_epoch`](#modified-process_epoch)
- [New `process_proposer_lookahead`](#new-process_proposer_lookahead)

<!-- mdformat-toc end -->

Expand Down Expand Up @@ -77,3 +88,137 @@ def process_execution_payload(state: BeaconState, body: BeaconBlockBody, executi
excess_blob_gas=payload.excess_blob_gas,
)
```

## Containers

### Extended Containers

#### `BeaconState`

*Note*: The `BeaconState` container is extended with the `proposer_lookahead` field, which is a list of validator indices covering the full lookahead period, starting from the beginning of the current epoch. For example, `proposer_lookahead[0]` is the validator index for the first proposer in the current epoch, `proposer_lookahead[1]` is the validator index for the next proposer in the current epoch, and so forth. The length of the `proposer_lookahead` list is `(MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH`, reflecting how far ahead proposer indices are computed based on the `MIN_SEED_LOOKAHEAD` parameter.

```python
class BeaconState(Container):
# Versioning
genesis_time: uint64
genesis_validators_root: Root
slot: Slot
fork: Fork
# History
latest_block_header: BeaconBlockHeader
block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT]
historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT]
# Eth1
eth1_data: Eth1Data
eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH]
eth1_deposit_index: uint64
# Registry
validators: List[Validator, VALIDATOR_REGISTRY_LIMIT]
balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT]
# Randomness
randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR]
# Slashings
slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] # Per-epoch sums of slashed effective balances
# Participation
previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]
current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]
# Finality
justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] # Bit set for every recent justified epoch
previous_justified_checkpoint: Checkpoint
current_justified_checkpoint: Checkpoint
finalized_checkpoint: Checkpoint
# Inactivity
inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT]
# Sync
current_sync_committee: SyncCommittee
next_sync_committee: SyncCommittee
# Execution
latest_execution_payload_header: ExecutionPayloadHeader
# Withdrawals
next_withdrawal_index: WithdrawalIndex
next_withdrawal_validator_index: ValidatorIndex
# Deep history valid from Capella onwards
historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT]
deposit_requests_start_index: uint64
deposit_balance_to_consume: Gwei
exit_balance_to_consume: Gwei
earliest_exit_epoch: Epoch
consolidation_balance_to_consume: Gwei
earliest_consolidation_epoch: Epoch
pending_deposits: List[PendingDeposit, PENDING_DEPOSITS_LIMIT]
pending_partial_withdrawals: List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT]
pending_consolidations: List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT]
proposer_lookahead: List[ValidatorIndex, (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH] # [New in Fulu:EIP7917]
```

## Helper functions

### Misc

#### New `compute_proposer_indices`

```python
def compute_proposer_indices(state: BeaconState, epoch: Epoch) -> List[ValidatorIndex, SLOTS_PER_EPOCH]:
"""
Return the proposer indices for the given `epoch`.
"""
epoch_seed = get_seed(state, epoch, DOMAIN_BEACON_PROPOSER)
start_slot = compute_start_slot_at_epoch(epoch)
seeds = [hash(epoch_seed + uint_to_bytes(Slot(start_slot + i))) for i in range(SLOTS_PER_EPOCH)]
indices = get_active_validator_indices(state, epoch)
return [compute_proposer_index(state, indices, seed) for seed in seeds]
```

### Beacon state accessors

#### Modified `get_beacon_proposer_index`

*Note*: The function `get_beacon_proposer_index` is modified to use the pre-calculated `current_proposer_lookahead` instead of calculating it on-demand.

```python
def get_beacon_proposer_index(state: BeaconState) -> ValidatorIndex:
"""
Return the beacon proposer index at the current slot.
"""
return state.proposer_lookahead[state.slot % SLOTS_PER_EPOCH]
```

### Epoch processing

#### Modified `process_epoch`

*Note*: The function `process_epoch` is modified in Fulu to call `process_proposer_lookahead` to update the `proposer_lookahead` in the beacon state.

```python
def process_epoch(state: BeaconState) -> None:
process_justification_and_finalization(state)
process_inactivity_updates(state)
process_rewards_and_penalties(state)
process_registry_updates(state)
process_slashings(state)
process_eth1_data_reset(state)
process_pending_deposits(state)
process_pending_consolidations(state)
process_effective_balance_updates(state)
process_slashings_reset(state)
process_randao_mixes_reset(state)
process_historical_summaries_update(state)
process_participation_flag_updates(state)
process_sync_committee_updates(state)
process_proposer_lookahead(state) # [New in Fulu:EIP7917]
```

#### New `process_proposer_lookahead`

*Note*: This function updates the `proposer_lookahead` field in the beacon state by shifting out proposer indices from the earliest epoch and appending new proposer indices for the latest epoch. With `MIN_SEED_LOOKAHEAD` set to `1`, this means that at the start of epoch `N`, the proposer lookahead for epoch `N+1` will be computed and included in the beacon state's lookahead.

```python
def process_proposer_lookahead(state: BeaconState) -> None:
last_epoch_start = len(state.proposer_lookahead) - SLOTS_PER_EPOCH
# Shift out proposers in the first epoch
state.proposer_lookahead[:last_epoch_start] = state.proposer_lookahead[SLOTS_PER_EPOCH:]
# Fill in the last epoch with new proposer indices
last_epoch_proposers = compute_proposer_indices(state, Epoch(get_current_epoch(state) + MIN_SEED_LOOKAHEAD + 1))
state.proposer_lookahead[last_epoch_start:] = last_epoch_proposers
```
19 changes: 19 additions & 0 deletions specs/fulu/fork.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- [Helper functions](#helper-functions)
- [Misc](#misc)
- [Modified `compute_fork_version`](#modified-compute_fork_version)
- [New `initialize_proposer_lookahead`](#new-initialize_proposer_lookahead)
- [Fork to Fulu](#fork-to-fulu)
- [Fork trigger](#fork-trigger)
- [Upgrading the state](#upgrading-the-state)
Expand Down Expand Up @@ -54,6 +55,23 @@ def compute_fork_version(epoch: Epoch) -> Version:
return GENESIS_FORK_VERSION
```

#### New `initialize_proposer_lookahead`

```python
def initialize_proposer_lookahead(
state: electra.BeaconState
) -> List[ValidatorIndex, (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH]:
"""
Return the proposer indices for the full available lookahead starting from current epoch.
Used to initialize the `proposer_lookahead` field in the beacon state at genesis and after forks.
"""
current_epoch = get_current_epoch(state)
lookahead = []
for i in range(MIN_SEED_LOOKAHEAD + 1):
lookahead.extend(compute_proposer_indices(state, Epoch(current_epoch + i)))
return lookahead
```

## Fork to Fulu

### Fork trigger
Expand Down Expand Up @@ -127,6 +145,7 @@ def upgrade_to_fulu(pre: electra.BeaconState) -> BeaconState:
pending_deposits=pre.pending_deposits,
pending_partial_withdrawals=pre.pending_partial_withdrawals,
pending_consolidations=pre.pending_consolidations,
proposer_lookahead=initialize_proposer_lookahead(pre), # [New in Fulu:EIP7917]
)

return post
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from eth2spec.test.context import (
spec_state_test,
with_phases,
)
from eth2spec.test.helpers.state import (
next_epoch,
)
from eth2spec.test.helpers.withdrawals import (
set_compounding_withdrawal_credential,
)
from eth2spec.test.helpers.attestations import (
state_transition_with_full_block,
)

from eth2spec.test.helpers.constants import ELECTRA, FULU
from eth2spec.test.helpers.state import simulate_lookahead


def run_test_effective_balance_increase_changes_lookahead(spec, state, expect_lookahead_changed):
# Advance few epochs to adjust the RANDAO
next_epoch(spec, state)
next_epoch(spec, state)
next_epoch(spec, state)

# Set all active validators to have balance close to the hysteresis threshold
current_epoch = spec.get_current_epoch(state)
active_validator_indices = spec.get_active_validator_indices(state, current_epoch)
for validator_index in active_validator_indices:
# Set compounding withdrawal credentials for the validator
set_compounding_withdrawal_credential(spec, state, validator_index)
state.validators[validator_index].effective_balance = 32000000000
# Set balance to close the next hysteresis threshold
state.balances[validator_index] = 33250000000 - 1

# Calculate the lookahead of next epoch
next_epoch_lookahead = simulate_lookahead(spec, state)[spec.SLOTS_PER_EPOCH :]

# Process 1-epoch worth of blocks with attestations
for _ in range(spec.SLOTS_PER_EPOCH):
_ = state_transition_with_full_block(spec, state, fill_cur_epoch=True, fill_prev_epoch=True)

# Calculate the actual lookahead
actual_lookahead = simulate_lookahead(spec, state)[: spec.SLOTS_PER_EPOCH]

if expect_lookahead_changed:
assert next_epoch_lookahead != actual_lookahead
else:
assert next_epoch_lookahead == actual_lookahead


@with_phases(phases=[ELECTRA, FULU])
@spec_state_test
def test_effective_balance_increase_changes_lookahead(spec, state):
if spec.fork == ELECTRA:
# Pre-EIP-7917, effective balance changes due to attestation rewards
# changes the next epoch's lookahead
run_test_effective_balance_increase_changes_lookahead(
spec, state, expect_lookahead_changed=True
)
else:
# Post-EIP-7917, effective balance changes due to attestation rewards
# do not change the next epoch's lookahead
run_test_effective_balance_increase_changes_lookahead(
spec, state, expect_lookahead_changed=False
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from eth2spec.test.context import spec_state_test, with_fulu_and_later
from eth2spec.test.helpers.epoch_processing import run_epoch_processing_with
from eth2spec.test.helpers.state import next_epoch


@with_fulu_and_later
@spec_state_test
def test_next_epoch_proposer_lookahead_shifted_to_front(spec, state):
"""Test that the next epoch proposer lookahead is shifted to the front at epoch transition."""
# Transition few epochs to pass the MIN_SEED_LOOKAHEAD
next_epoch(spec, state)
next_epoch(spec, state)
# Get initial lookahead
initial_lookahead = state.proposer_lookahead.copy()

# Run epoch processing
yield "pre", state
next_epoch(spec, state)
yield "post", state

# Verify lookahead was shifted correctly
assert (
state.proposer_lookahead[: spec.SLOTS_PER_EPOCH]
== initial_lookahead[spec.SLOTS_PER_EPOCH :]
)


@with_fulu_and_later
@spec_state_test
def test_proposer_lookahead_in_state_matches_computed_lookahead(spec, state):
"""Test that the proposer lookahead in the state matches the lookahead computed on the fly."""
# Transition few epochs to pass the MIN_SEED_LOOKAHEAD
next_epoch(spec, state)
next_epoch(spec, state)

# Run epoch processing
yield "pre", state
next_epoch(spec, state)
yield "post", state

# Verify lookahead in state matches the lookahead computed on the fly
computed_lookahead = spec.initialize_proposer_lookahead(state)
assert state.proposer_lookahead == computed_lookahead
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from eth2spec.test.context import (
with_phases,
spec_test,
with_state,
)
from eth2spec.test.utils import with_meta_tags
from eth2spec.test.helpers.constants import (
ELECTRA,
FULU,
)
from eth2spec.test.helpers.fulu.fork import (
FULU_FORK_TEST_META_TAGS,
run_fork_test,
)
from eth2spec.test.helpers.state import next_slot


@with_phases(phases=[ELECTRA], other_phases=[FULU])
@spec_test
@with_state
@with_meta_tags(FULU_FORK_TEST_META_TAGS)
def test_lookahead_consistency_at_fork(spec, phases, state):
"""
Test that lookahead is consistent before/after the Fulu fork.
"""

# Calculate the current and next epoch lookahead by simulating the state progression
# with empty slots and calling `get_beacon_proposer_index` (how it was done pre-Fulu)
pre_fork_proposers = []
simulation_state = state.copy()
for _ in range(spec.SLOTS_PER_EPOCH * (spec.MIN_SEED_LOOKAHEAD + 1)):
proposer_index = spec.get_beacon_proposer_index(simulation_state)
pre_fork_proposers.append(proposer_index)
next_slot(spec, simulation_state)

# Upgrade to Fulu
spec = phases[FULU]
state = yield from run_fork_test(spec, state)

# Check if the pre-fork simulation matches the post-fork `state.proposer_lookahead`
assert pre_fork_proposers == state.proposer_lookahead
6 changes: 6 additions & 0 deletions tests/core/pyspec/eth2spec/test/helpers/genesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
compute_whisk_initial_k_commitment_cached,
)

from eth2spec.test.helpers.forks import is_post_fulu


def build_mock_validator(spec, i: int, balance: int):
active_pubkey = pubkeys[i]
Expand Down Expand Up @@ -222,4 +224,8 @@ def create_genesis_state(spec, validator_balances, activation_threshold):
state.latest_execution_payload_header.block_hash
) # last block is full

if is_post_fulu(spec):
# Initialize proposer lookahead list
state.proposer_lookahead = spec.initialize_proposer_lookahead(state)

return state
14 changes: 14 additions & 0 deletions tests/core/pyspec/eth2spec/test/helpers/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,17 @@ def get_validator_index_by_pubkey(state, pubkey):
def advance_finality_to(spec, state, epoch):
while state.finalized_checkpoint.epoch < epoch:
next_epoch_with_full_participation(spec, state)


def simulate_lookahead(spec, state):
"""
Simulate the lookahead by advancing the state forward with empty slots and
calling `get_beacon_proposer_index`.
"""
lookahead = []
simulation_state = state.copy()
for _ in range(spec.SLOTS_PER_EPOCH * (spec.MIN_SEED_LOOKAHEAD + 1)):
proposer_index = spec.get_beacon_proposer_index(simulation_state)
lookahead.append(proposer_index)
next_slot(spec, simulation_state)
return lookahead
8 changes: 6 additions & 2 deletions tests/generators/epoch_processing/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,12 @@
_new_electra_mods = {**_new_electra_mods_1, **_new_electra_mods_2}
electra_mods = combine_mods(_new_electra_mods, deneb_mods)

# No additional Fulu specific epoch processing tests
fulu_mods = electra_mods
# Fulu specific epoch processing tests
_new_fulu_mods = {
key: "eth2spec.test.fulu.epoch_processing.test_" + key
for key in ["process_proposer_lookahead", "effective_balance_increase_changes_lookahead"]
}
fulu_mods = combine_mods(_new_fulu_mods, electra_mods)

# TODO Custody Game testgen is disabled for now
# custody_game_mods = {**{key: 'eth2spec.test.custody_game.epoch_processing.test_process_' + key for key in [
Expand Down
Loading