diff --git a/.gitignore b/.gitignore index 82026c27bd..bffbbba428 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ tests/core/pyspec/eth2spec/bellatrix/ tests/core/pyspec/eth2spec/capella/ tests/core/pyspec/eth2spec/deneb/ tests/core/pyspec/eth2spec/eip6110/ +tests/core/pyspec/eth2spec/maxeb/ # coverage reports .htmlcov diff --git a/Makefile b/Makefile index ab5521663a..1c887a5409 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ MARKDOWN_FILES = $(wildcard $(SPEC_DIR)/*/*.md) \ $(wildcard $(SPEC_DIR)/_features/*/*/*.md) \ $(wildcard $(SSZ_DIR)/*.md) -ALL_EXECUTABLE_SPECS = phase0 altair bellatrix capella deneb eip6110 +ALL_EXECUTABLE_SPECS = phase0 altair bellatrix capella deneb eip6110 maxeb # The parameters for commands. Use `foreach` to avoid listing specs again. COVERAGE_SCOPE := $(foreach S,$(ALL_EXECUTABLE_SPECS), --cov=eth2spec.$S.$(TEST_PRESET_TYPE)) PYLINT_SCOPE := $(foreach S,$(ALL_EXECUTABLE_SPECS), ./eth2spec/$S) diff --git a/configs/mainnet.yaml b/configs/mainnet.yaml index 5ad394c082..5ea2b69aba 100644 --- a/configs/mainnet.yaml +++ b/configs/mainnet.yaml @@ -53,6 +53,9 @@ DENEB_FORK_EPOCH: 18446744073709551615 # EIP6110 EIP6110_FORK_VERSION: 0x05000000 # temporary stub EIP6110_FORK_EPOCH: 18446744073709551615 +# MAXEB +MAXEB_FORK_VERSION: 0x06000000 # temporary stub +MAXEB_FORK_EPOCH: 18446744073709551615 # Time parameters diff --git a/configs/minimal.yaml b/configs/minimal.yaml index 5895cfc707..368f1138c4 100644 --- a/configs/minimal.yaml +++ b/configs/minimal.yaml @@ -52,6 +52,9 @@ DENEB_FORK_EPOCH: 18446744073709551615 # EIP6110 EIP6110_FORK_VERSION: 0x05000001 EIP6110_FORK_EPOCH: 18446744073709551615 +# MAXEB +MAXEB_FORK_VERSION: 0x06000000 # temporary stub +MAXEB_FORK_EPOCH: 18446744073709551615 # Time parameters diff --git a/presets/mainnet/maxeb.yaml b/presets/mainnet/maxeb.yaml new file mode 100644 index 0000000000..c5ae0d0c08 --- /dev/null +++ b/presets/mainnet/maxeb.yaml @@ -0,0 +1,8 @@ +# Mainnet preset - MAXEB + +# Gwei values +# --------------------------------------------------------------- +# 2**5 * 10**9 (= 32,000,000,000) Gwei +MIN_ACTIVATION_BALANCE: 32000000000 +# 2**11 * 10**9 (= 2,048,000,000,000) Gwei +MAX_EFFECTIVE_BALANCE_MAXEB: 2048000000000 diff --git a/presets/minimal/maxeb.yaml b/presets/minimal/maxeb.yaml new file mode 100644 index 0000000000..bb4e2fbe0c --- /dev/null +++ b/presets/minimal/maxeb.yaml @@ -0,0 +1,8 @@ +# Minimal preset - MAXEB + +# Gwei values +# --------------------------------------------------------------- +# 2**5 * 10**9 (= 32,000,000,000) Gwei +MIN_ACTIVATION_BALANCE: 32000000000 +# 2**11 * 10**9 (= 2,048,000,000,000) Gwei +MAX_EFFECTIVE_BALANCE_MAXEB: 2048000000000 diff --git a/setup.py b/setup.py index 36b92d5125..1d86d515f4 100644 --- a/setup.py +++ b/setup.py @@ -48,6 +48,7 @@ def installPackage(package: str): CAPELLA = 'capella' DENEB = 'deneb' EIP6110 = 'eip6110' +MAXEB = 'maxeb' # The helper functions that are used when defining constants @@ -680,10 +681,22 @@ def imports(cls, preset_name: str): from eth2spec.deneb import {preset_name} as deneb ''' +# +# MAXEBSpecBuilder +# +class MAXEBSpecBuilder(DenebSpecBuilder): + fork: str = MAXEB + + @classmethod + def imports(cls, preset_name: str): + return super().imports(preset_name) + f''' +from eth2spec.deneb import {preset_name} as deneb +''' + spec_builders = { builder.fork: builder - for builder in (Phase0SpecBuilder, AltairSpecBuilder, BellatrixSpecBuilder, CapellaSpecBuilder, DenebSpecBuilder, EIP6110SpecBuilder) + for builder in (Phase0SpecBuilder, AltairSpecBuilder, BellatrixSpecBuilder, CapellaSpecBuilder, DenebSpecBuilder, EIP6110SpecBuilder, MAXEBSpecBuilder) } @@ -982,14 +995,14 @@ def finalize_options(self): if len(self.md_doc_paths) == 0: print("no paths were specified, using default markdown file paths for pyspec" " build (spec fork: %s)" % self.spec_fork) - if self.spec_fork in (PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, EIP6110): + if self.spec_fork in (PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, EIP6110, MAXEB): self.md_doc_paths = """ specs/phase0/beacon-chain.md specs/phase0/fork-choice.md specs/phase0/validator.md specs/phase0/weak-subjectivity.md """ - if self.spec_fork in (ALTAIR, BELLATRIX, CAPELLA, DENEB, EIP6110): + if self.spec_fork in (ALTAIR, BELLATRIX, CAPELLA, DENEB, EIP6110, MAXEB): self.md_doc_paths += """ specs/altair/light-client/full-node.md specs/altair/light-client/light-client.md @@ -1001,7 +1014,7 @@ def finalize_options(self): specs/altair/validator.md specs/altair/p2p-interface.md """ - if self.spec_fork in (BELLATRIX, CAPELLA, DENEB, EIP6110): + if self.spec_fork in (BELLATRIX, CAPELLA, DENEB, EIP6110, MAXEB): self.md_doc_paths += """ specs/bellatrix/beacon-chain.md specs/bellatrix/fork.md @@ -1010,7 +1023,7 @@ def finalize_options(self): specs/bellatrix/p2p-interface.md sync/optimistic.md """ - if self.spec_fork in (CAPELLA, DENEB, EIP6110): + if self.spec_fork in (CAPELLA, DENEB, EIP6110, MAXEB): self.md_doc_paths += """ specs/capella/light-client/fork.md specs/capella/light-client/full-node.md @@ -1022,7 +1035,7 @@ def finalize_options(self): specs/capella/validator.md specs/capella/p2p-interface.md """ - if self.spec_fork in (DENEB, EIP6110): + if self.spec_fork in (DENEB, EIP6110, MAXEB): self.md_doc_paths += """ specs/deneb/light-client/fork.md specs/deneb/light-client/full-node.md @@ -1044,6 +1057,11 @@ def finalize_options(self): specs/_features/eip6110/beacon-chain.md specs/_features/eip6110/fork.md """ + if self.spec_fork == MAXEB: + self.md_doc_paths += """ + specs/_features/maxeb/beacon-chain.md + specs/_features/maxeb/fork.md + """ if len(self.md_doc_paths) == 0: raise Exception('no markdown files specified, and spec fork "%s" is unknown', self.spec_fork) diff --git a/specs/_features/maxeb/beacon-chain.md b/specs/_features/maxeb/beacon-chain.md new file mode 100644 index 0000000000..23cae6116a --- /dev/null +++ b/specs/_features/maxeb/beacon-chain.md @@ -0,0 +1,577 @@ +# MAXEB - Spec + +## Table of contents + + + + + +- [Introduction](#introduction) +- [Constants](#constants) + - [Withdrawal prefixes](#withdrawal-prefixes) + - [Gwei values](#gwei-values) +- [Containers](#containers) + - [New containers](#new-containers) + - [Extended Containers](#extended-containers) + - [`BeaconState`](#beaconstate) +- [Helpers](#helpers) + - [Predicates](#predicates) + - [updated `is_eligible_for_activation_queue`](#updated-is_eligible_for_activation_queue) + - [new `has_compounding_withdrawal_credential`](#new-has_compounding_withdrawal_credential) + - [updated `is_fully_withdrawable_validator`](#updated--is_fully_withdrawable_validator) + - [new `get_validator_excess_balance`](#new-get_validator_excess_balance) + - [updated `is_partially_withdrawable_validator`](#updated--is_partially_withdrawable_validator) + - [Beacon state accessors](#beacon-state-accessors) + - [updated `get_validator_churn_limit`](#updated--get_validator_churn_limit) + - [Beacon state mutators](#beacon-state-mutators) + - [updated `initiate_validator_exit`](#updated--initiate_validator_exit) +- [Genesis](#genesis) + - [updated `initialize_beacon_state_from_eth1`](#updated--initialize_beacon_state_from_eth1) +- [Beacon chain state transition function](#beacon-chain-state-transition-function) + - [Epoch processing](#epoch-processing) + - [updated `process_registry_updates`](#updated--process_registry_updates) + - [Block processing](#block-processing) + - [updated `get_expected_withdrawals`](#updated--get_expected_withdrawals) + - [Operations](#operations) + - [Deposits](#deposits) + + + + +## Introduction + +See [a modest proposal](https://notes.ethereum.org/@mikeneuder/increase-maxeb), the [diff view](https://github.com/michaelneuder/consensus-specs/pull/3/files) and +[security considerations](https://notes.ethereum.org/@fradamt/meb-increase-security). + +*Note:* This specification is built upon [Deneb](../../deneb/beacon-chain.md). + +## Constants + +The following values are (non-configurable) constants used throughout the specification. + +### Withdrawal prefixes + +| Name | Value | +| - | - | +| `BLS_WITHDRAWAL_PREFIX` | `Bytes1('0x00')` | +| `ETH1_ADDRESS_WITHDRAWAL_PREFIX` | `Bytes1('0x01')` | +| `COMPOUNDING_WITHDRAWAL_PREFIX` | `Bytes1('0x02')` | + +### Gwei values + +| Name | Value | +| - | - | +| `MIN_ACTIVATION_BALANCE` | `Gwei(2**5 * 10**9)` (32 ETH) | +| `MAX_EFFECTIVE_BALANCE_MAXEB` | `Gwei(2**11 * 10**9)` (2048 ETH) | + +## Containers + +### New containers + +#### new `PendingBalanceDeposit` + +```python +class PendingBalanceDeposit(Container): + index: ValidatorIndex + amount: Gwei +``` + +#### new `PartialWithdrawal` + +```python +class PartialWithdrawal(Container): + index: ValidatorIndex + amount: Gwei + withdrawable_epoch: Epoch +``` +#### new `ExecutionLayerWithdrawRequest` + +```python +class ExecutionLayerWithdrawRequest(Container): + source_address: ExecutionAddress + validator_pubkey: BLSPubkey + balance: Gwei +``` + +### Extended Containers + +#### `BeaconState` + +*Note*: adding `activation_validator_balance` and `exit_queue_churn` fields to +aid in rate limiting of activation and exit queue based on ETH rather than +validator counts. + +```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] + # --- NEW --- # + deposit_balance_to_consume: Gwei + exit_balance_to_consume: Gwei # Should be initialized with get_validator_churn_limit(state) + earliest_exit_epoch: Epoch # Should be initialized with the max([v.exit_epoch for v in state.validators if v.exit_epoch != FAR_FUTURE_EPOCH]) + 1 + pending_balance_deposits: List[PendingBalanceDeposit] + pending_partial_withdrawals: List[PartialWithdrawal] +``` + +## Helpers + +### Predicates + +#### updated `is_eligible_for_activation_queue` + +*Note*: Use `>= MIN_ACTIVATION_BALANCE` instead of `== MAX_EFFECTIVE_BALANCE` + +```python +def is_eligible_for_activation_queue(validator: Validator) -> bool: + """ + Check if ``validator`` is eligible to be placed into the activation queue. + """ + return ( + validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH + # --- MODIFIED --- # + and validator.effective_balance >= MIN_ACTIVATION_BALANCE + # --- END MODIFIED --- # + ) +``` + +#### new `has_compounding_withdrawal_credential` + +```python +def has_compounding_withdrawal_credential(validator: Validator) -> bool: + """ + Check if ``validator`` has an 0x02 prefixed "compounding" withdrawal credential. + """ + return validator.withdrawal_credentials[:1] == COMPOUNDING_WITHDRAWAL_PREFIX +``` + +#### updated `is_fully_withdrawable_validator` + +*Note*: now calls `has_compounding_withdrawal_credential` too. + +```python +def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool: + """ + Check if ``validator`` is fully withdrawable. + """ + return ( + # --- MODIFIED --- # + (has_eth1_withdrawal_credential(validator) or has_compounding_withdrawal_credential(validator)) + # --- END MODIFIED --- # + and validator.withdrawable_epoch <= epoch + and balance > 0 + ) +``` + +#### updated `is_partially_withdrawable_validator` +*Note*: now calls `has_withdrawalable_credential` and gets ceiling from `get_balance_ceiling`. + +```python +def is_partially_withdrawable_validator(validator: Validator, balance: Gwei) -> bool: + """ + Check if ``validator`` is partially withdrawable. + """ + # --- MODIFIED --- # + return get_validator_excess_balance(validator, balance) > 0 + # --- END MODIFIED --- # +``` + + +### Beacon state accessors + + +#### new `get_validator_excess_balance` + +```python +def get_validator_excess_balance(validator: Validator, balance: Gwei) -> Gwei: + """ + Get excess balance for partial withdrawals for ``validator``. + """ + if has_compounding_withdrawal_credential(validator) and balance > MAX_EFFECTIVE_BALANCE_MAXEB: + return balance - MAX_EFFECTIVE_BALANCE_MAXEB + elif has_eth1_withdrawal_credential(validator) and balance > MIN_ACTIVATION_BALANCE: + return balance - MIN_ACTIVATION_BALANCE + return Gwei(0) +``` + +#### updated `get_validator_churn_limit` + +*Note*: updated to return a Gwei amount of amount of churn per epoch. + +```python +def get_validator_churn_limit(state: BeaconState) -> Gwei: + churn = max(config.MIN_PER_EPOCH_CHURN_LIMIT * MIN_ACTIVATION_BALANCE, get_total_active_balance(state) // config.CHURN_LIMIT_QUOTIENT) + return churn - churn % EFFECTIVE_BALANCE_INCREMENT +``` + +### Beacon state mutators + +#### updated `initiate_validator_exit` + +*Note*: Modification to make validator exits constrained by the balance +of the exiting validators. + +```python +def initiate_validator_exit(state: BeaconState, index: ValidatorIndex) -> None: + """ + Initiate the exit of the validator with index ``index``. + """ + # Return if validator already initiated exit + validator = state.validators[index] + if validator.exit_epoch != FAR_FUTURE_EPOCH: + return + + # Compute exit queue epoch + exit_queue_epoch = compute_exit_epoch_and_update_churn(state, validator.effective_balance) + + # Set validator exit epoch and withdrawable epoch + validator.exit_epoch = exit_queue_epoch + validator.withdrawable_epoch = Epoch(validator.exit_epoch + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY) +``` + +#### new `compute_exit_epoch_and_update_churn` + + +```python +def compute_exit_epoch_and_update_churn(state: BeaconState, exit_balance: Gwei) -> Epoch: + earliest_exit_epoch = compute_activation_exit_epoch(get_current_epoch(state)) + per_epoch_churn = get_validator_churn_limit(state) + # New epoch for exits. + if state.earliest_exit_epoch < earliest_exit_epoch: + state.earliest_exit_epoch = earliest_exit_epoch + state.exit_balance_to_consume = per_epoch_churn + + # Exit fits in the current earliest epoch. + if exit_balance <= state.exit_balance_to_consume: + state.exit_balance_to_consume -= exit_balance + else: # Exit doesn't fit in the current earliest epoch. + balance_to_process = exit_balance - state.exit_balance_to_consume + additional_epochs, remainder = divmod(balance_to_process, per_epoch_churn) + state.earliest_exit_epoch += additional_epochs + 1 + state.exit_balance_to_consume = per_epoch_churn - remainder + return state.earliest_exit_epoch +``` + +## Genesis +#### updated `initialize_beacon_state_from_eth1` +*Note*: Replace `== MAX_EFFECTIVE_BALANCE` with `>= MIN_ACTIVATION_BALANCE` when +checking for activation processing. +```python +def initialize_beacon_state_from_eth1(eth1_block_hash: Hash32, + eth1_timestamp: uint64, + deposits: Sequence[Deposit]) -> BeaconState: + fork = Fork( + previous_version=GENESIS_FORK_VERSION, + current_version=GENESIS_FORK_VERSION, + epoch=GENESIS_EPOCH, + ) + state = BeaconState( + genesis_time=eth1_timestamp + GENESIS_DELAY, + fork=fork, + eth1_data=Eth1Data(block_hash=eth1_block_hash, deposit_count=uint64(len(deposits))), + latest_block_header=BeaconBlockHeader(body_root=hash_tree_root(BeaconBlockBody())), + randao_mixes=[eth1_block_hash] * EPOCHS_PER_HISTORICAL_VECTOR, # Seed RANDAO with Eth1 entropy + ) + # Process deposits + leaves = list(map(lambda deposit: deposit.data, deposits)) + for index, deposit in enumerate(deposits): + deposit_data_list = List[DepositData, 2**DEPOSIT_CONTRACT_TREE_DEPTH](*leaves[:index + 1]) + state.eth1_data.deposit_root = hash_tree_root(deposit_data_list) + process_deposit(state, deposit) + # Process activations + for index, validator in enumerate(state.validators): + balance = state.balances[index] + validator.effective_balance = min(balance - balance % EFFECTIVE_BALANCE_INCREMENT, MAX_EFFECTIVE_BALANCE_MAXEB) + + # --- MODIFIED --- # + if validator.effective_balance >= MIN_ACTIVATION_BALANCE: + validator.activation_eligibility_epoch = GENESIS_EPOCH + validator.activation_epoch = GENESIS_EPOCH + # --- END MODIFIED --- # + # Set genesis validators root for domain separation and chain versioning + state.genesis_validators_root = hash_tree_root(state.validators) + return state +``` + +#### updated `get_validator_from_deposit` + +```python +def get_validator_from_deposit(pubkey: BLSPubkey, withdrawal_credentials: Bytes32) -> Validator: + return Validator( + pubkey=pubkey, + withdrawal_credentials=withdrawal_credentials, + activation_eligibility_epoch=FAR_FUTURE_EPOCH, + activation_epoch=FAR_FUTURE_EPOCH, + exit_epoch=FAR_FUTURE_EPOCH, + withdrawable_epoch=FAR_FUTURE_EPOCH, + effective_balance=0, + ) +``` + +## Beacon chain state transition function +### Epoch processing + +#### updated `process_epoch` +```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_balance_deposits(state) # New + process_effective_balance_updates(state) + process_slashings_reset(state) + process_randao_mixes_reset(state) +``` + +#### updated `process_registry_updates` +*Note*: changing the dequed validators to depend on the weight of activation up to the +churn limit. +```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)) + + # Activate all eligible validators + activation_epoch = compute_activation_exit_epoch(get_current_epoch(state)) + for validator in state.validators: + if is_eligible_for_activation(state, validator): + validator.activation_epoch = activation_epoch +``` + +#### new `process_pending_balance_deposits` + +```python +def process_pending_balance_deposits(state: BeaconState) -> None: + state.deposit_balance_to_consume += get_validator_churn_limit(state) + next_pending_deposit_index = 0 + for pending_balance_deposit in state.pending_balance_deposits: + if state.deposit_balance_to_consume < pending_balance_deposit.amount: + break + + state.deposit_balance_to_consume -= pending_balance_deposit.amount + increase_balance(state, pending_balance_deposit.index, pending_balance_deposit.amount) + next_pending_deposit_index += 1 + + state.pending_balance_deposits = state.pending_balance_deposits[next_pending_deposit_index:] +``` + +#### updated `process_effective_balance_updates` + +```python +def process_effective_balance_updates(state: BeaconState) -> None: + # Update effective balances with hysteresis + for index, validator in enumerate(state.validators): + balance = state.balances[index] + HYSTERESIS_INCREMENT = uint64(EFFECTIVE_BALANCE_INCREMENT // HYSTERESIS_QUOTIENT) + DOWNWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_DOWNWARD_MULTIPLIER + UPWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_UPWARD_MULTIPLIER + EFFECTIVE_BALANCE_LIMIT = MAX_EFFECTIVE_BALANCE_MAXEB if has_compounding_withdrawal_credential(validator) else MIN_ACTIVATION_BALANCE + if ( + balance + DOWNWARD_THRESHOLD < validator.effective_balance + or validator.effective_balance + UPWARD_THRESHOLD < balance + ): + validator.effective_balance = min(balance - balance % EFFECTIVE_BALANCE_INCREMENT, EFFECTIVE_BALANCE_LIMIT) +``` + +### Block processing + +#### updated `process_operations` + +```python +def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: + # Verify that outstanding deposits are processed up to the maximum number of deposits + assert len(body.deposits) == min(MAX_DEPOSITS, state.eth1_data.deposit_count - state.eth1_deposit_index) + def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: + for operation in operations: + fn(state, operation) + for_ops(body.proposer_slashings, process_proposer_slashing) + for_ops(body.attester_slashings, process_attester_slashing) + for_ops(body.attestations, process_attestation) + for_ops(body.deposits, process_deposit) + for_ops(body.voluntary_exits, process_voluntary_exit) + for_ops(body.bls_to_execution_changes, process_bls_to_execution_change) + for_ops(body.execution_payload.withdraw_request, process_execution_layer_withdraw_request) # New +``` + +#### new `process_execution_layer_withdraw_request` + +```python +def process_execution_layer_withdraw_request( + state: BeaconState, + execution_layer_withdraw_request: ExecutionLayerWithdrawRequest + ) -> None: + validator_pubkeys = [v.pubkey for v in state.validators] + validator_index = ValidatorIndex(validator_pubkeys.index(execution_layer_withdraw_request.validator_pubkey)) + validator = state.validators[validator_index] + + # Same conditions as in EIP7002 https://github.com/ethereum/consensus-specs/pull/3349/files#diff-7a6e2ba480d22d8bd035bd88ca91358456caf9d7c2d48a74e1e900fe63d5c4f8R223 + # Verify withdrawal credentials + is_execution_address = validator.withdrawal_credentials[:1] == ETH1_ADDRESS_WITHDRAWAL_PREFIX + is_correct_source_address = validator.withdrawal_credentials[12:] == execution_layer_withdraw_request.source_address + if not (is_execution_address and is_correct_source_address): + return + # Verify the validator is active + if not is_active_validator(validator, get_current_epoch(state)): + return + # Verify exit has not been initiated, and slashed + if validator.exit_epoch != FAR_FUTURE_EPOCH: + return + # Verify the validator has been active long enough + if get_current_epoch(state) < validator.activation_epoch + config.SHARD_COMMITTEE_PERIOD: + return + + pending_balance_to_withdraw = sum(item.amount for item in state.pending_partial_withdrawals if item.index == validator_index) + # TODO: Should substract `MIN_ACTIVATION_BALANCE` or ejection balance? + available_balance = state.balances[validator_index] - MIN_ACTIVATION_BALANCE - pending_balance_to_withdraw + if available_balance < execution_layer_withdraw_request.balance: + return + + exit_queue_epoch = compute_exit_epoch_and_update_churn(state, available_balance) + withdrawable_epoch = Epoch(exit_queue_epoch + config.MIN_VALIDATOR_WITHDRAWABILITY_DELAY) + + state.pending_partial_withdrawals.append(PartialWithdrawal( + index=validator_index, + amount=available_balance, + withdrawable_epoch=withdrawable_epoch, + )) +``` + + +#### updated `get_expected_withdrawals` + +```python +def get_expected_withdrawals(state: BeaconState) -> Sequence[Withdrawal]: + epoch = get_current_epoch(state) + withdrawal_index = state.next_withdrawal_index + validator_index = state.next_withdrawal_validator_index + withdrawals: List[Withdrawal] = [] + consumed = 0 + for withdrawal in state.pending_partial_withdrawals: + if withdrawal.withdrawable_epoch > epoch or len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD // 2: + break + + validator = state.validators[withdrawal.index] + if validator.exit_epoch == FAR_FUTURE_EPOCH and state.balances[withdrawal.index] > MIN_ACTIVATION_BALANCE: + withdrawable_balance = min(state.balances[withdrawal.index] - MIN_ACTIVATION_BALANCE, withdrawal.amount) + withdrawals.append(Withdrawal( + index=withdrawal_index, + validator_index=withdrawal.index, + address=ExecutionAddress(validator.withdrawal_credentials[12:]), + amount=withdrawable_balance, + )) + withdrawal_index += WithdrawalIndex(1) + consumed += 1 + + state.pending_partial_withdrawals = state.pending_partial_withdrawals[consumed:] + + # Sweep for remaining. + bound = min(len(state.validators), MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) + for _ in range(bound): + validator = state.validators[validator_index] + balance = state.balances[validator_index] + if is_fully_withdrawable_validator(validator, balance, epoch): + withdrawals.append(Withdrawal( + index=withdrawal_index, + validator_index=validator_index, + address=ExecutionAddress(validator.withdrawal_credentials[12:]), + amount=balance, + )) + withdrawal_index += WithdrawalIndex(1) + elif is_partially_withdrawable_validator(validator, balance): + withdrawals.append(Withdrawal( + index=withdrawal_index, + validator_index=validator_index, + address=ExecutionAddress(validator.withdrawal_credentials[12:]), + amount=get_validator_excess_balance(validator, balance), + )) + withdrawal_index += WithdrawalIndex(1) + if len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD: + break + validator_index = ValidatorIndex((validator_index + 1) % len(state.validators)) + return withdrawals +``` + +#### Operations + +##### Deposits + +**updated `apply_deposit`** + +*Note*: updated to cap top-offs at 32 ETH to avoid skipping activation queue. + +```python +def apply_deposit(state: BeaconState, + pubkey: BLSPubkey, + withdrawal_credentials: Bytes32, + amount: uint64, + signature: BLSSignature) -> None: + validator_pubkeys = [validator.pubkey for validator in state.validators] + if pubkey not in validator_pubkeys: + # Verify the deposit signature (proof of possession) which is not checked by the deposit contract + deposit_message = DepositMessage( + pubkey=pubkey, + withdrawal_credentials=withdrawal_credentials, + amount=amount, + ) + domain = compute_domain(DOMAIN_DEPOSIT) # Fork-agnostic domain since deposits are valid across forks + signing_root = compute_signing_root(deposit_message, domain) + # Initialize validator if the deposit signature is valid + if bls.Verify(pubkey, signing_root, signature): + state.validators.append(get_validator_from_deposit(pubkey, withdrawal_credentials)) + state.balances.append(0) + # [New in Altair] + state.previous_epoch_participation.append(ParticipationFlags(0b0000_0000)) + state.current_epoch_participation.append(ParticipationFlags(0b0000_0000)) + state.inactivity_scores.append(uint64(0)) + else: + index = ValidatorIndex(validator_pubkeys.index(pubkey)) + state.pending_balance_deposits.append(PendingBalanceDeposit(index, amount)) +``` \ No newline at end of file diff --git a/specs/_features/maxeb/fork.md b/specs/_features/maxeb/fork.md new file mode 100644 index 0000000000..8522068236 --- /dev/null +++ b/specs/_features/maxeb/fork.md @@ -0,0 +1,125 @@ +# MAXEB -- Fork Logic + +**Notice**: This document is a work-in-progress for researchers and implementers. + +## Table of contents + + + + +- [Introduction](#introduction) +- [Configuration](#configuration) +- [Helper functions](#helper-functions) + - [Misc](#misc) + - [Modified `compute_fork_version`](#modified-compute_fork_version) +- [Fork to EIP7045](#fork-to-eip7045) + - [Fork trigger](#fork-trigger) + - [Upgrading the state](#upgrading-the-state) + + + +## Introduction + +This document describes the process of MAXEB upgrade. + +## Configuration + +Warning: this configuration is not definitive. + +| Name | Value | +| - | - | +| `MAXEB_FORK_VERSION` | `Version('0x06000000')` | +| `MAXEB_FORK_EPOCH` | `Epoch(18446744073709551615)` **TBD** | + +## Helper functions + +### Misc + +#### Modified `compute_fork_version` + +```python +def compute_fork_version(epoch: Epoch) -> Version: + """ + Return the fork version at the given ``epoch``. + """ + if epoch >= MAXEB_FORK_EPOCH: + return MAXEB_FORK_VERSION + if epoch >= DENEB_FORK_EPOCH: + return DENEB_FORK_VERSION + if epoch >= CAPELLA_FORK_EPOCH: + return CAPELLA_FORK_VERSION + if epoch >= BELLATRIX_FORK_EPOCH: + return BELLATRIX_FORK_VERSION + if epoch >= ALTAIR_FORK_EPOCH: + return ALTAIR_FORK_VERSION + return GENESIS_FORK_VERSION +``` + +## Fork to MAXEB + +### Fork trigger + +TBD. This fork is defined for testing purposes, the EIP may be combined with other consensus-layer upgrade. +For now, we assume the condition will be triggered at epoch `MAXEB_FORK_EPOCH`. + +Note that for the pure MAXEB networks, we don't apply `upgrade_to_maxeb` since it starts with MAXEB version logic. + +### Upgrading the state + +If `state.slot % SLOTS_PER_EPOCH == 0` and `compute_epoch_at_slot(state.slot) == MAXEB_FORK_EPOCH`, +an irregular state change is made to upgrade to MAXEB. + +```python +def upgrade_to_maxeb(pre: deneb.BeaconState) -> BeaconState: + post = BeaconState( + # Versioning + genesis_time=pre.genesis_time, + genesis_validators_root=pre.genesis_validators_root, + slot=pre.slot, + fork=Fork( + previous_version=pre.fork.current_version, + current_version=MAXEB_FORK_VERSION, + epoch=deneb.get_current_epoch(pre), + ), + # History + latest_block_header=pre.latest_block_header, + block_roots=pre.block_roots, + state_roots=pre.state_roots, + historical_roots=pre.historical_roots, + # Eth1 + eth1_data=pre.eth1_data, + eth1_data_votes=pre.eth1_data_votes, + eth1_deposit_index=pre.eth1_deposit_index, + # Registry + validators=pre.validators, + balances=pre.balances, + # new MAXEB and set to default values. + activation_validator_balance=0, + exit_queue_churn=0, + # Randomness + randao_mixes=pre.randao_mixes, + # Slashings + slashings=pre.slashings, + # Participation + previous_epoch_participation=pre.previous_epoch_participation, + current_epoch_participation=pre.current_epoch_participation, + # Finality + justification_bits=pre.justification_bits, + previous_justified_checkpoint=pre.previous_justified_checkpoint, + current_justified_checkpoint=pre.current_justified_checkpoint, + finalized_checkpoint=pre.finalized_checkpoint, + # Inactivity + inactivity_scores=pre.inactivity_scores, + # Sync + current_sync_committee=pre.current_sync_committee, + next_sync_committee=pre.next_sync_committee, + # Execution-layer + latest_execution_payload_header=pre.latest_execution_payload_header, + # Withdrawals + next_withdrawal_index=pre.next_withdrawal_index, + next_withdrawal_validator_index=pre.next_withdrawal_validator_index, + # Deep history valid from Capella onwards + historical_summaries=pre.historical_summaries, + ) + return post +``` \ No newline at end of file diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 626ffc1dbc..878b7d1671 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -9,12 +9,13 @@ from eth2spec.capella import mainnet as spec_capella_mainnet, minimal as spec_capella_minimal from eth2spec.deneb import mainnet as spec_deneb_mainnet, minimal as spec_deneb_minimal from eth2spec.eip6110 import mainnet as spec_eip6110_mainnet, minimal as spec_eip6110_minimal +from eth2spec.maxeb import mainnet as spec_maxeb_mainnet, minimal as spec_maxeb_minimal from eth2spec.utils import bls from .exceptions import SkippedTest from .helpers.constants import ( PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, - EIP6110, + EIP6110, MAXEB, MINIMAL, MAINNET, ALL_PHASES, ALL_FORK_UPGRADES, @@ -82,6 +83,7 @@ class ForkMeta: CAPELLA: spec_capella_minimal, DENEB: spec_deneb_minimal, EIP6110: spec_eip6110_minimal, + MAXEB: spec_maxeb_minimal, }, MAINNET: { PHASE0: spec_phase0_mainnet, @@ -90,6 +92,7 @@ class ForkMeta: CAPELLA: spec_capella_mainnet, DENEB: spec_deneb_mainnet, EIP6110: spec_eip6110_mainnet, + MAXEB: spec_maxeb_mainnet, }, } @@ -433,6 +436,7 @@ def decorator(fn): with_capella_and_later = with_all_phases_from(CAPELLA) with_deneb_and_later = with_all_phases_from(DENEB) with_eip6110_and_later = with_all_phases_from(EIP6110) +with_maxeb_and_later = with_all_phases_from(MAXEB) def _get_preset_targets(kw): diff --git a/tests/core/pyspec/eth2spec/test/helpers/constants.py b/tests/core/pyspec/eth2spec/test/helpers/constants.py index 2140c96e45..0ba5219d61 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/constants.py +++ b/tests/core/pyspec/eth2spec/test/helpers/constants.py @@ -16,16 +16,17 @@ CUSTODY_GAME = SpecForkName('custody_game') DAS = SpecForkName('das') EIP6110 = SpecForkName('eip6110') +MAXEB = SpecForkName('maxeb') # The forks that pytest can run with. ALL_PHASES = ( # Formal forks PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, # Experimental patches - EIP6110, + EIP6110, MAXEB ) # The forks that output to the test vectors. -TESTGEN_FORKS = (PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, EIP6110) +TESTGEN_FORKS = (PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, EIP6110, MAXEB) ALL_FORK_UPGRADES = { # pre_fork_name: post_fork_name @@ -33,7 +34,7 @@ ALTAIR: BELLATRIX, BELLATRIX: CAPELLA, CAPELLA: DENEB, - DENEB: EIP6110, + DENEB: MAXEB, } ALL_PRE_POST_FORKS = ALL_FORK_UPGRADES.items() AFTER_BELLATRIX_UPGRADES = {key: value for key, value in ALL_FORK_UPGRADES.items() if key != PHASE0} diff --git a/tests/core/pyspec/eth2spec/test/helpers/forks.py b/tests/core/pyspec/eth2spec/test/helpers/forks.py index 5e97522dbb..7a04d83006 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/forks.py +++ b/tests/core/pyspec/eth2spec/test/helpers/forks.py @@ -1,10 +1,12 @@ from .constants import ( PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, - EIP6110, + EIP6110, MAXEB ) def is_post_fork(a, b): + if a == MAXEB: + return b in [PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, MAXEB] if a == EIP6110: return b in [PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, EIP6110] if a == DENEB: @@ -38,3 +40,7 @@ def is_post_deneb(spec): def is_post_eip6110(spec): return is_post_fork(spec.fork, EIP6110) + + +def is_post_maxeb(spec): + return is_post_fork(spec.fork, MAXEB) diff --git a/tests/core/pyspec/eth2spec/test/helpers/genesis.py b/tests/core/pyspec/eth2spec/test/helpers/genesis.py index fea259013b..879a0f14fe 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/genesis.py +++ b/tests/core/pyspec/eth2spec/test/helpers/genesis.py @@ -1,11 +1,11 @@ from eth2spec.test.helpers.constants import ( - ALTAIR, BELLATRIX, CAPELLA, DENEB, EIP6110, + ALTAIR, BELLATRIX, CAPELLA, DENEB, EIP6110, MAXEB ) from eth2spec.test.helpers.execution_payload import ( compute_el_header_block_hash, ) from eth2spec.test.helpers.forks import ( - is_post_altair, is_post_bellatrix, is_post_capella, is_post_eip6110, + is_post_altair, is_post_bellatrix, is_post_capella, is_post_eip6110, is_post_maxeb ) from eth2spec.test.helpers.keys import pubkeys @@ -86,6 +86,9 @@ def create_genesis_state(spec, validator_balances, activation_threshold): elif spec.fork == EIP6110: previous_version = spec.config.DENEB_FORK_VERSION current_version = spec.config.EIP6110_FORK_VERSION + elif spec.fork == MAXEB: + previous_version = spec.config.DENEB_FORK_VERSION + current_version = spec.config.MAXEB_FORK_VERSION state = spec.BeaconState( genesis_time=0, @@ -137,5 +140,9 @@ def create_genesis_state(spec, validator_balances, activation_threshold): if is_post_eip6110(spec): state.deposit_receipts_start_index = spec.UNSET_DEPOSIT_RECEIPTS_START_INDEX + + if is_post_maxeb(spec): + state.activation_validator_balance = 0 + state.exit_queue_churn = 0 return state diff --git a/tests/core/pyspec/eth2spec/test/maxeb/__init__.py b/tests/core/pyspec/eth2spec/test/maxeb/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/core/pyspec/eth2spec/test/maxeb/block_processing/__init__.py b/tests/core/pyspec/eth2spec/test/maxeb/block_processing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/core/pyspec/eth2spec/test/maxeb/block_processing/test_max_eb.py b/tests/core/pyspec/eth2spec/test/maxeb/block_processing/test_max_eb.py new file mode 100644 index 0000000000..cd12913be7 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/maxeb/block_processing/test_max_eb.py @@ -0,0 +1,192 @@ +from eth2spec.test.context import ( + spec_state_test, + with_maxeb_and_later, +) + +# ******************** +# * EXIT QUEUE TESTS * +# ******************** + +@with_maxeb_and_later +@spec_state_test +def test_exit_queue_churn_32eth_validators(spec, state): + # This state has 64 validators each with 32 ETH + single_validator_balance = spec.MIN_ACTIVATION_BALANCE + expected_exit_epoch = spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) + + # Withdraw validators, all which fit in the churn limit + for i in range(spec.config.MIN_PER_EPOCH_CHURN_LIMIT): + validator_index = i + spec.initiate_validator_exit(state, validator_index) + # Check exit queue churn is set + assert state.exit_queue_churn == single_validator_balance * (i + 1) + # Check exit epoch + assert state.validators[validator_index].exit_epoch == expected_exit_epoch + + # Withdraw an additional validator, doesn't fit in the churn limit, so exit + # epoch is incremented + validator_index = spec.config.MIN_PER_EPOCH_CHURN_LIMIT + spec.initiate_validator_exit(state, validator_index) + # Check exit epoch + assert state.validators[validator_index].exit_epoch == expected_exit_epoch + 1 + # Check exit queue churn is set + assert state.exit_queue_churn == single_validator_balance + + +@with_maxeb_and_later +@spec_state_test +def test_exit_queue_churn_large_validator(spec, state): + cl = spec.get_validator_churn_limit(state) + assert cl == spec.MIN_ACTIVATION_BALANCE * spec.config.MIN_PER_EPOCH_CHURN_LIMIT + + # Set 0th validator effective balance to 2048 ETH + state.validators[0].effective_balance = spec.MAX_EFFECTIVE_BALANCE_MAXEB + + expected_exit_epoch = spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) + # Validator consumes exit churn for an additional 16 epochs + expected_exit_epoch += spec.MAX_EFFECTIVE_BALANCE_MAXEB // cl + + validator_index = 0 + spec.initiate_validator_exit(state, validator_index) + # Check exit epoch + assert state.validators[validator_index].exit_epoch == expected_exit_epoch + # Check exit queue churn is 0 + assert state.exit_queue_churn == 0 + + +@with_maxeb_and_later +@spec_state_test +def test_exit_queue_churn_large_validator_existing_churn(spec, state): + cl = spec.get_validator_churn_limit(state) + assert cl == spec.MIN_ACTIVATION_BALANCE * spec.config.MIN_PER_EPOCH_CHURN_LIMIT + + # Set the churn to 1 ETH + state.exit_queue_churn = 1000000000 + + # Set 0th validator effective balance to the churn limit + state.validators[0].effective_balance = cl + + expected_exit_epoch = spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) + # The existing 1 ETH churn will push an extra epoch + expected_exit_epoch += 1 + + validator_index = 0 + spec.initiate_validator_exit(state, validator_index) + # Check exit epoch + assert state.validators[validator_index].exit_epoch == expected_exit_epoch + # Check exit queue churn is the remainder 1 ETH + assert state.exit_queue_churn == 1000000000 + + +@with_maxeb_and_later +@spec_state_test +def test_exit_queue_churn_large_validator_existing_churn_2epochs(spec, state): + cl = spec.get_validator_churn_limit(state) + assert cl == spec.MIN_ACTIVATION_BALANCE * spec.config.MIN_PER_EPOCH_CHURN_LIMIT + + # Set the churn to 1 ETH. + state.exit_queue_churn = 1000000000 + + # Set 0th validator effective balance to the churn limit + state.validators[0].effective_balance = 2*cl + + expected_exit_epoch = spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) + # Two extra epochs will be necessary + expected_exit_epoch += 2 + + validator_index = 0 + spec.initiate_validator_exit(state, validator_index) + # Check exit epoch + assert state.validators[validator_index].exit_epoch == expected_exit_epoch + # Check exit queue churn is the remainder 1 ETH + assert state.exit_queue_churn == 1000000000 + + + +# ************************** +# * ACTIVATION QUEUE TESTS * +# ************************** + +def mark_validators_eligible_for_activation(spec, state): + for v in state.validators: + v.activation_eligibility_epoch = 0 + v.activation_epoch = spec.FAR_FUTURE_EPOCH + +@with_maxeb_and_later +@spec_state_test +def test_activation_queue_churn_32eth_validators(spec, state): + mark_validators_eligible_for_activation(spec, state) + + expected_activation_epoch = spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) + + # Activate validators, 4 should fit + spec.process_registry_updates(state) + + for i in range(spec.config.MIN_PER_EPOCH_CHURN_LIMIT): + # Check exit epoch + assert state.validators[i].activation_epoch == expected_activation_epoch + + # Check activation validator balance is 0 + assert state.activation_validator_balance == 0 + + # Check that the next validator has not been dequeued + assert state.validators[spec.config.MIN_PER_EPOCH_CHURN_LIMIT+1].activation_epoch == spec.FAR_FUTURE_EPOCH + +@with_maxeb_and_later +@spec_state_test +def test_activation_queue_churn_large_validator(spec, state): + mark_validators_eligible_for_activation(spec, state) + + cl = spec.get_validator_churn_limit(state) + assert cl == spec.MIN_ACTIVATION_BALANCE * spec.config.MIN_PER_EPOCH_CHURN_LIMIT + + # Set 0th validator effective balance to 2048 ETH + state.validators[0].effective_balance = spec.MAX_EFFECTIVE_BALANCE_MAXEB + + # Process updates, should just consume the churn limit + assert state.activation_validator_balance == 0 + spec.process_registry_updates(state) + assert state.activation_validator_balance == cl + # Activation epoch should not be set + assert state.validators[0].activation_epoch == spec.FAR_FUTURE_EPOCH + + # Validator consumes activation churn for an additional 16 epochs + activation_epochs_consumed = spec.MAX_EFFECTIVE_BALANCE_MAXEB // cl + for i in range(1, activation_epochs_consumed-1): + spec.process_registry_updates(state) + + # Balance should be one churn limit away + assert state.activation_validator_balance == cl * (activation_epochs_consumed-1) + # Activation epoch should still not be set + assert state.validators[0].activation_epoch == spec.FAR_FUTURE_EPOCH + + # Process updates, dequeues the validator + spec.process_registry_updates(state) + assert state.activation_validator_balance == 0 + # Activation epoch is now set + assert state.validators[0].activation_epoch == spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) + + # Check that the next validator has not been dequeued + assert state.validators[1].activation_epoch == spec.FAR_FUTURE_EPOCH + +@with_maxeb_and_later +@spec_state_test +def test_activation_queue_churn_existing_churn(spec, state): + mark_validators_eligible_for_activation(spec, state) + + cl = spec.get_validator_churn_limit(state) + assert cl == spec.MIN_ACTIVATION_BALANCE * spec.config.MIN_PER_EPOCH_CHURN_LIMIT + + # Set 0th validator effective balance to churn limit + 1 + state.validators[0].effective_balance = cl + 1000000000 + # Set the activation validator balance to 1 ETH. + state.activation_validator_balance = 1000000000 + + # Process updates, should activate the validator + spec.process_registry_updates(state) + assert state.activation_validator_balance == 0 + # Activation epoch should be set + assert state.validators[0].activation_epoch == spec.compute_activation_exit_epoch(spec.get_current_epoch(state)) + + # Check that the next validator has not been dequeued + assert state.validators[1].activation_epoch == spec.FAR_FUTURE_EPOCH \ No newline at end of file diff --git a/tests/generators/genesis/main.py b/tests/generators/genesis/main.py index feffde8e38..89ef935220 100644 --- a/tests/generators/genesis/main.py +++ b/tests/generators/genesis/main.py @@ -1,5 +1,5 @@ from eth2spec.gen_helpers.gen_from_tests.gen import run_state_test_generators, combine_mods -from eth2spec.test.helpers.constants import PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, EIP6110 +from eth2spec.test.helpers.constants import PHASE0, ALTAIR, BELLATRIX, CAPELLA, DENEB, EIP6110, MAXEB if __name__ == "__main__":