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

Add max epoch activation churn limit (EIP-7514) to Deneb #3499

Merged
merged 15 commits into from
Sep 18, 2023
3 changes: 2 additions & 1 deletion configs/mainnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ EJECTION_BALANCE: 16000000000
MIN_PER_EPOCH_CHURN_LIMIT: 4
# 2**16 (= 65,536)
CHURN_LIMIT_QUOTIENT: 65536

# [New in Deneb:EIP7514] 2**3 (= 8)
MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 8

# Fork choice
# ---------------------------------------------------------------
Expand Down
6 changes: 4 additions & 2 deletions configs/minimal.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,12 @@ INACTIVITY_SCORE_BIAS: 4
INACTIVITY_SCORE_RECOVERY_RATE: 16
# 2**4 * 10**9 (= 16,000,000,000) Gwei
EJECTION_BALANCE: 16000000000
# 2**2 (= 4)
MIN_PER_EPOCH_CHURN_LIMIT: 4
# [customized] more easily demonstrate the difference between this value and the activation churn limit
MIN_PER_EPOCH_CHURN_LIMIT: 2
# [customized] scale queue churn at much lower validator counts for testing
CHURN_LIMIT_QUOTIENT: 32
# [New in Deneb:EIP7514] [customized]
MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT: 4


# Fork choice
Expand Down
53 changes: 53 additions & 0 deletions specs/deneb/beacon-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- [Preset](#preset)
- [Execution](#execution)
- [Configuration](#configuration)
- [Validator cycle](#validator-cycle)
- [Containers](#containers)
- [Extended containers](#extended-containers)
- [`BeaconBlockBody`](#beaconblockbody)
Expand All @@ -26,6 +27,7 @@
- [`kzg_commitment_to_versioned_hash`](#kzg_commitment_to_versioned_hash)
- [Beacon state accessors](#beacon-state-accessors)
- [Modified `get_attestation_participation_flag_indices`](#modified-get_attestation_participation_flag_indices)
- [New `get_validator_activation_churn_limit`](#new-get_validator_activation_churn_limit)
- [Beacon chain state transition function](#beacon-chain-state-transition-function)
- [Execution engine](#execution-engine)
- [Request data](#request-data)
Expand All @@ -40,6 +42,8 @@
- [Execution payload](#execution-payload)
- [Modified `process_execution_payload`](#modified-process_execution_payload)
- [Modified `process_voluntary_exit`](#modified-process_voluntary_exit)
- [Epoch processing](#epoch-processing)
- [Registry updates](#registry-updates)
- [Testing](#testing)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Expand All @@ -52,6 +56,7 @@ Deneb is a consensus-layer upgrade containing a number of features. Including:
* [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844): Shard Blob Transactions scale data-availability of Ethereum in a simple, forwards-compatible manner
* [EIP-7044](https://eips.ethereum.org/EIPS/eip-7044): Perpetually Valid Signed Voluntary Exits
* [EIP-7045](https://eips.ethereum.org/EIPS/eip-7045): Increase Max Attestation Inclusion Slot
* [EIP-7514](https://eips.ethereum.org/EIPS/eip-7514): Add Max Epoch Churn Limit

## Custom types

Expand Down Expand Up @@ -89,6 +94,12 @@ and are limited by `MAX_BLOB_GAS_PER_BLOCK // GAS_PER_BLOB`. However the CL limi

## Configuration

### Validator cycle

| Name | Value |
| - | - |
| `MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT` | `uint64(2**3)` (= 8) |
Copy link
Contributor

@tbenr tbenr Sep 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a nit: since this new param is only applied from Deneb, should it be called DENEB_MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT ?

There might be confusion in testnets with a Bellatrix genesis and a MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT set to 1 and you may think that it is immediately applied but you actually have to wait until Deneb

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, but it would break the current code conventions for the new constants/presets/configurations. 🤔

Copy link
Contributor

@tbenr tbenr Sep 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 ... It actually should be MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT_DENEB, similar to
MAX_REQUEST_BLOCKS_DENEB, even if the phase0 counterpart MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT won't exist.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, but we do have MAX_REQUEST_BLOCKS in phase0! Adding a suffix to a new preset breaks the current code conventions.


## Containers

### Extended containers
Expand Down Expand Up @@ -211,6 +222,16 @@ def get_attestation_participation_flag_indices(state: BeaconState,
return participation_flag_indices
```

#### New `get_validator_activation_churn_limit`

```python
def get_validator_activation_churn_limit(state: BeaconState) -> uint64:
"""
Return the validator activation churn limit for the current epoch.
"""
return min(MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT, get_validator_churn_limit(state))
```

## Beacon chain state transition function

### Execution engine
Expand Down Expand Up @@ -415,6 +436,38 @@ def process_voluntary_exit(state: BeaconState, signed_voluntary_exit: SignedVolu
initiate_validator_exit(state, voluntary_exit.validator_index)
```

### Epoch processing

#### Registry updates

*Note*: The function `process_registry_updates` is modified to utilize `get_validator_activation_churn_limit()` to rate limit the activation queue for EIP-7514.

```python
def process_registry_updates(state: BeaconState) -> None:
# Process activation eligibility and ejections
for index, validator in enumerate(state.validators):
if is_eligible_for_activation_queue(validator):
validator.activation_eligibility_epoch = get_current_epoch(state) + 1

if (
is_active_validator(validator, get_current_epoch(state))
and validator.effective_balance <= EJECTION_BALANCE
):
initiate_validator_exit(state, ValidatorIndex(index))

# Queue validators eligible for activation and not yet dequeued for activation
activation_queue = sorted([
index for index, validator in enumerate(state.validators)
if is_eligible_for_activation(state, validator)
# Order by the sequence of activation_eligibility_epoch setting and then index
], key=lambda index: (state.validators[index].activation_eligibility_epoch, index))
# Dequeued validators for activation up to activation churn limit
# [Modified in Deneb:EIP7514]
for index in activation_queue[:get_validator_activation_churn_limit(state)]:
validator = state.validators[index]
validator.activation_epoch = compute_activation_exit_epoch(get_current_epoch(state))
```

## Testing

*Note*: The function `initialize_beacon_state_from_eth1` is modified for pure Deneb testing only.
Expand Down
26 changes: 23 additions & 3 deletions tests/core/pyspec/eth2spec/test/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,34 @@ def default_balances(spec: Spec):
return [spec.MAX_EFFECTIVE_BALANCE] * num_validators


def scaled_churn_balances(spec: Spec):
def scaled_churn_balances_min_churn_limit(spec: Spec):
"""
Helper method to create enough validators to scale the churn limit.
(This is *firmly* over the churn limit -- thus the +2 instead of just +1)
See the second argument of ``max`` in ``get_validator_churn_limit``.
Usage: `@with_custom_state(balances_fn=scaled_churn_balances, ...)`
Usage: `@with_custom_state(balances_fn=scaled_churn_balances_min_churn_limit, ...)`
"""
num_validators = spec.config.CHURN_LIMIT_QUOTIENT * (2 + spec.config.MIN_PER_EPOCH_CHURN_LIMIT)
num_validators = spec.config.CHURN_LIMIT_QUOTIENT * (spec.config.MIN_PER_EPOCH_CHURN_LIMIT + 2)
return [spec.MAX_EFFECTIVE_BALANCE] * num_validators


def scaled_churn_balances_equal_activation_churn_limit(spec: Spec):
"""
Helper method to create enough validators to scale the churn limit.
(This is *firmly* over the churn limit -- thus the +2 instead of just +1)
Usage: `@with_custom_state(balances_fn=scaled_churn_balances_exceed_activation_churn_limit, ...)`
"""
num_validators = spec.config.CHURN_LIMIT_QUOTIENT * (spec.config.MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT)
return [spec.MAX_EFFECTIVE_BALANCE] * num_validators


def scaled_churn_balances_exceed_activation_churn_limit(spec: Spec):
"""
Helper method to create enough validators to scale the churn limit.
(This is *firmly* over the churn limit -- thus the +2 instead of just +1)
Usage: `@with_custom_state(balances_fn=scaled_churn_balances_exceed_activation_churn_limit, ...)`
"""
num_validators = spec.config.CHURN_LIMIT_QUOTIENT * (spec.config.MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT + 2)
return [spec.MAX_EFFECTIVE_BALANCE] * num_validators


Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from eth2spec.test.helpers.keys import pubkeys
from eth2spec.test.helpers.constants import MINIMAL
from eth2spec.test.context import (
with_deneb_and_later,
spec_test,
spec_state_test,
single_phase,
with_custom_state,
with_presets,
scaled_churn_balances_exceed_activation_churn_limit,
scaled_churn_balances_equal_activation_churn_limit,
)
from eth2spec.test.helpers.epoch_processing import run_epoch_processing_with


def run_process_registry_updates(spec, state):
yield from run_epoch_processing_with(spec, state, 'process_registry_updates')


def run_test_activation_churn_limit(spec, state):
mock_activations = spec.get_validator_activation_churn_limit(state) * 2

validator_count_0 = len(state.validators)

for i in range(mock_activations):
index = validator_count_0 + i
validator = spec.Validator(
pubkey=pubkeys[index],
withdrawal_credentials=spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX + b'\x00' * 11 + b'\x56' * 20,
activation_eligibility_epoch=0,
activation_epoch=spec.FAR_FUTURE_EPOCH,
exit_epoch=spec.FAR_FUTURE_EPOCH,
withdrawable_epoch=spec.FAR_FUTURE_EPOCH,
effective_balance=spec.MAX_EFFECTIVE_BALANCE,
)
state.validators.append(validator)
state.balances.append(spec.MAX_EFFECTIVE_BALANCE)
state.previous_epoch_participation.append(spec.ParticipationFlags(0b0000_0000))
state.current_epoch_participation.append(spec.ParticipationFlags(0b0000_0000))
state.inactivity_scores.append(0)
state.validators[index].activation_epoch = spec.FAR_FUTURE_EPOCH

churn_limit_0 = spec.get_validator_activation_churn_limit(state)

yield from run_process_registry_updates(spec, state)

# Half should churn in first run of registry update
for i in range(mock_activations):
index = validator_count_0 + i
if index < validator_count_0 + churn_limit_0:
# The eligible validators within the activation churn limit should have been activated
assert state.validators[index].activation_epoch < spec.FAR_FUTURE_EPOCH
else:
assert state.validators[index].activation_epoch == spec.FAR_FUTURE_EPOCH


@with_deneb_and_later
@with_presets([MINIMAL],
reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated")
@spec_test
@with_custom_state(balances_fn=scaled_churn_balances_exceed_activation_churn_limit,
threshold_fn=lambda spec: spec.config.EJECTION_BALANCE)
@single_phase
def test_activation_churn_limit__greater_than_activation_limit(spec, state):
assert spec.get_validator_activation_churn_limit(state) == spec.config.MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT
assert spec.get_validator_churn_limit(state) > spec.config.MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT
yield from run_test_activation_churn_limit(spec, state)


@with_deneb_and_later
@with_presets([MINIMAL],
reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated")
@spec_test
@with_custom_state(balances_fn=scaled_churn_balances_equal_activation_churn_limit,
threshold_fn=lambda spec: spec.config.EJECTION_BALANCE)
@single_phase
def test_activation_churn_limit__equal_to_activation_limit(spec, state):
assert spec.get_validator_activation_churn_limit(state) == spec.config.MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT
assert spec.get_validator_churn_limit(state) == spec.config.MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT
yield from run_test_activation_churn_limit(spec, state)


@with_deneb_and_later
@with_presets([MINIMAL],
reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated")
@spec_state_test
def test_activation_churn_limit__less_than_activation_limit(spec, state):
assert spec.get_validator_activation_churn_limit(state) < spec.config.MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT
assert spec.get_validator_churn_limit(state) < spec.config.MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT
yield from run_test_activation_churn_limit(spec, state)
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
spec_state_test,
always_bls, with_all_phases, with_presets,
spec_test, single_phase,
with_custom_state, scaled_churn_balances,
with_custom_state,
scaled_churn_balances_min_churn_limit,
)
from eth2spec.test.helpers.keys import pubkey_to_privkey
from eth2spec.test.helpers.voluntary_exits import (
Expand Down Expand Up @@ -102,7 +103,8 @@ def test_success_exit_queue__min_churn(spec, state):
@with_presets([MINIMAL],
reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated")
@spec_test
@with_custom_state(balances_fn=scaled_churn_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE)
@with_custom_state(balances_fn=scaled_churn_balances_min_churn_limit,
threshold_fn=lambda spec: spec.config.EJECTION_BALANCE)
@single_phase
def test_success_exit_queue__scaled_churn(spec, state):
churn_limit = spec.get_validator_churn_limit(state)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
spec_test, spec_state_test,
with_all_phases, single_phase,
with_custom_state, with_presets,
scaled_churn_balances,
scaled_churn_balances_min_churn_limit,
)
from eth2spec.test.helpers.epoch_processing import run_epoch_processing_with

Expand Down Expand Up @@ -164,7 +164,8 @@ def test_activation_queue_efficiency_min(spec, state):
@with_presets([MINIMAL],
reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated")
@spec_test
@with_custom_state(balances_fn=scaled_churn_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE)
@with_custom_state(balances_fn=scaled_churn_balances_min_churn_limit,
threshold_fn=lambda spec: spec.config.EJECTION_BALANCE)
@single_phase
def test_activation_queue_efficiency_scaled(spec, state):
assert spec.get_validator_churn_limit(state) > spec.config.MIN_PER_EPOCH_CHURN_LIMIT
Expand Down Expand Up @@ -227,7 +228,8 @@ def test_ejection_past_churn_limit_min(spec, state):
@with_presets([MINIMAL],
reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated")
@spec_test
@with_custom_state(balances_fn=scaled_churn_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE)
@with_custom_state(balances_fn=scaled_churn_balances_min_churn_limit,
threshold_fn=lambda spec: spec.config.EJECTION_BALANCE)
@single_phase
def test_ejection_past_churn_limit_scaled(spec, state):
assert spec.get_validator_churn_limit(state) > spec.config.MIN_PER_EPOCH_CHURN_LIMIT
Expand Down Expand Up @@ -324,7 +326,8 @@ def test_activation_queue_activation_and_ejection__exceed_churn_limit(spec, stat
@with_presets([MINIMAL],
reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated")
@spec_test
@with_custom_state(balances_fn=scaled_churn_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE)
@with_custom_state(balances_fn=scaled_churn_balances_min_churn_limit,
threshold_fn=lambda spec: spec.config.EJECTION_BALANCE)
@single_phase
def test_activation_queue_activation_and_ejection__scaled_churn_limit(spec, state):
churn_limit = spec.get_validator_churn_limit(state)
Expand All @@ -336,7 +339,8 @@ def test_activation_queue_activation_and_ejection__scaled_churn_limit(spec, stat
@with_presets([MINIMAL],
reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated")
@spec_test
@with_custom_state(balances_fn=scaled_churn_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE)
@with_custom_state(balances_fn=scaled_churn_balances_min_churn_limit,
threshold_fn=lambda spec: spec.config.EJECTION_BALANCE)
@single_phase
def test_activation_queue_activation_and_ejection__exceed_scaled_churn_limit(spec, state):
churn_limit = spec.get_validator_churn_limit(state)
Expand Down
5 changes: 4 additions & 1 deletion tests/generators/epoch_processing/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@
]}
capella_mods = combine_mods(_new_capella_mods, bellatrix_mods)

deneb_mods = capella_mods
_new_deneb_mods = {key: 'eth2spec.test.deneb.epoch_processing.test_process_' + key for key in [
'registry_updates',
]}
deneb_mods = combine_mods(_new_deneb_mods, capella_mods)

eip6110_mods = deneb_mods

Expand Down