diff --git a/configs/mainnet.yaml b/configs/mainnet.yaml index bcd18e5cd..252f82dbe 100644 --- a/configs/mainnet.yaml +++ b/configs/mainnet.yaml @@ -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 # --------------------------------------------------------------- diff --git a/configs/minimal.yaml b/configs/minimal.yaml index d23ca7adb..a3b1a8d5a 100644 --- a/configs/minimal.yaml +++ b/configs/minimal.yaml @@ -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 diff --git a/specs/deneb/beacon-chain.md b/specs/deneb/beacon-chain.md index 50c2248d1..b98ac1259 100644 --- a/specs/deneb/beacon-chain.md +++ b/specs/deneb/beacon-chain.md @@ -16,6 +16,7 @@ - [Preset](#preset) - [Execution](#execution) - [Configuration](#configuration) + - [Validator cycle](#validator-cycle) - [Containers](#containers) - [Extended containers](#extended-containers) - [`BeaconBlockBody`](#beaconblockbody) @@ -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) @@ -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) @@ -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 @@ -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) | + ## Containers ### Extended containers @@ -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 @@ -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. diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 0c9d4a1ec..7289fdf0f 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -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 diff --git a/tests/core/pyspec/eth2spec/test/deneb/epoch_processing/__init__.py b/tests/core/pyspec/eth2spec/test/deneb/epoch_processing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/core/pyspec/eth2spec/test/deneb/epoch_processing/test_process_registry_updates.py b/tests/core/pyspec/eth2spec/test/deneb/epoch_processing/test_process_registry_updates.py new file mode 100644 index 000000000..4cbcc1ed5 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/deneb/epoch_processing/test_process_registry_updates.py @@ -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) diff --git a/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_voluntary_exit.py b/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_voluntary_exit.py index 4a7286d52..97208dfcd 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_voluntary_exit.py +++ b/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_voluntary_exit.py @@ -3,7 +3,8 @@ from eth2spec.test.context import ( 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 ( @@ -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) diff --git a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_registry_updates.py b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_registry_updates.py index b4c5f81a0..b7a7be76a 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_registry_updates.py +++ b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_registry_updates.py @@ -5,7 +5,7 @@ from eth2spec.test.context import ( 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 @@ -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 @@ -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 @@ -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) @@ -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) diff --git a/tests/generators/epoch_processing/main.py b/tests/generators/epoch_processing/main.py index 645c84cb6..63c2a548f 100644 --- a/tests/generators/epoch_processing/main.py +++ b/tests/generators/epoch_processing/main.py @@ -32,7 +32,10 @@ if __name__ == "__main__": ]} 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