diff --git a/.circleci/config.yml b/.circleci/config.yml index 31da1db0f3..bdb3f5bc66 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -129,6 +129,20 @@ jobs: command: make citest fork=bellatrix - store_test_results: path: tests/core/pyspec/test-reports + test-capella: + docker: + - image: circleci/python:3.8 + working_directory: ~/specs-repo + steps: + - restore_cache: + key: v3-specs-repo-{{ .Branch }}-{{ .Revision }} + - restore_pyspec_cached_venv + - run: + name: Run py-tests + command: make citest fork=capella + - store_test_results: + path: tests/core/pyspec/test-reports + table_of_contents: docker: - image: circleci/node:10.16.3 @@ -243,6 +257,9 @@ workflows: - test-bellatrix: requires: - install_pyspec_test + - test-capella: + requires: + - install_pyspec_test - table_of_contents - codespell - lint: diff --git a/.gitignore b/.gitignore index 243d099bfb..101cb0b086 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ consensus-spec-tests/ tests/core/pyspec/eth2spec/phase0/ tests/core/pyspec/eth2spec/altair/ tests/core/pyspec/eth2spec/bellatrix/ +tests/core/pyspec/eth2spec/capella/ # coverage reports .htmlcov diff --git a/Makefile b/Makefile index 7de4cec2a0..17aac0cc24 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,7 @@ GENERATOR_VENVS = $(patsubst $(GENERATOR_DIR)/%, $(GENERATOR_DIR)/%venv, $(GENER MARKDOWN_FILES = $(wildcard $(SPEC_DIR)/phase0/*.md) $(wildcard $(SPEC_DIR)/altair/*.md) $(wildcard $(SSZ_DIR)/*.md) \ $(wildcard $(SPEC_DIR)/bellatrix/*.md) \ + $(wildcard $(SPEC_DIR)/capella/*.md) \ $(wildcard $(SPEC_DIR)/custody/*.md) \ $(wildcard $(SPEC_DIR)/das/*.md) \ $(wildcard $(SPEC_DIR)/sharding/*.md) \ @@ -98,12 +99,12 @@ install_test: # Testing against `minimal` config by default test: pyspec . venv/bin/activate; cd $(PY_SPEC_DIR); \ - python3 -m pytest -n 4 --disable-bls --cov=eth2spec.phase0.minimal --cov=eth2spec.altair.minimal --cov=eth2spec.bellatrix.minimal --cov-report="html:$(COV_HTML_OUT)" --cov-branch eth2spec + python3 -m pytest -n 4 --disable-bls --cov=eth2spec.phase0.minimal --cov=eth2spec.altair.minimal --cov=eth2spec.bellatrix.minimal --cov=eth2spec.capella.minimal --cov-report="html:$(COV_HTML_OUT)" --cov-branch eth2spec # Testing against `minimal` config by default find_test: pyspec . venv/bin/activate; cd $(PY_SPEC_DIR); \ - python3 -m pytest -k=$(K) --disable-bls --cov=eth2spec.phase0.minimal --cov=eth2spec.altair.minimal --cov=eth2spec.bellatrix.minimal --cov-report="html:$(COV_HTML_OUT)" --cov-branch eth2spec + python3 -m pytest -k=$(K) --disable-bls --cov=eth2spec.phase0.minimal --cov=eth2spec.altair.minimal --cov=eth2spec.bellatrix.minimal --cov=eth2spec.capella.minimal --cov-report="html:$(COV_HTML_OUT)" --cov-branch eth2spec citest: pyspec mkdir -p tests/core/pyspec/test-reports/eth2spec; @@ -136,7 +137,7 @@ lint: pyspec . venv/bin/activate; cd $(PY_SPEC_DIR); \ flake8 --config $(LINTER_CONFIG_FILE) ./eth2spec \ && pylint --disable=all --enable unused-argument ./eth2spec/phase0 ./eth2spec/altair ./eth2spec/bellatrix \ - && mypy --config-file $(LINTER_CONFIG_FILE) -p eth2spec.phase0 -p eth2spec.altair -p eth2spec.bellatrix + && mypy --config-file $(LINTER_CONFIG_FILE) -p eth2spec.phase0 -p eth2spec.altair -p eth2spec.bellatrix -p eth2spec.capella lint_generators: pyspec . venv/bin/activate; cd $(TEST_GENERATORS_DIR); \ diff --git a/configs/mainnet.yaml b/configs/mainnet.yaml index f123f4f818..5b2046654d 100644 --- a/configs/mainnet.yaml +++ b/configs/mainnet.yaml @@ -44,11 +44,16 @@ ALTAIR_FORK_EPOCH: 74240 # Oct 27, 2021, 10:56:23am UTC # Bellatrix BELLATRIX_FORK_VERSION: 0x02000000 BELLATRIX_FORK_EPOCH: 18446744073709551615 +# Capella +CAPELLA_FORK_VERSION: 0x03000000 +CAPELLA_FORK_EPOCH: 18446744073709551615 # Sharding -SHARDING_FORK_VERSION: 0x03000000 +SHARDING_FORK_VERSION: 0x04000000 SHARDING_FORK_EPOCH: 18446744073709551615 + + # Time parameters # --------------------------------------------------------------- # 12 seconds diff --git a/configs/minimal.yaml b/configs/minimal.yaml index 61deb0be43..0910a1430d 100644 --- a/configs/minimal.yaml +++ b/configs/minimal.yaml @@ -43,8 +43,11 @@ ALTAIR_FORK_EPOCH: 18446744073709551615 # Bellatrix BELLATRIX_FORK_VERSION: 0x02000001 BELLATRIX_FORK_EPOCH: 18446744073709551615 +# Capella +CAPELLA_FORK_VERSION: 0x03000001 +CAPELLA_FORK_EPOCH: 18446744073709551615 # Sharding -SHARDING_FORK_VERSION: 0x03000001 +SHARDING_FORK_VERSION: 0x04000001 SHARDING_FORK_EPOCH: 18446744073709551615 diff --git a/presets/mainnet/capella.yaml b/presets/mainnet/capella.yaml new file mode 100644 index 0000000000..c5dfe1d4b8 --- /dev/null +++ b/presets/mainnet/capella.yaml @@ -0,0 +1 @@ +# Minimal preset - Capella diff --git a/presets/minimal/capella.yaml b/presets/minimal/capella.yaml new file mode 100644 index 0000000000..c5dfe1d4b8 --- /dev/null +++ b/presets/minimal/capella.yaml @@ -0,0 +1 @@ +# Minimal preset - Capella diff --git a/setup.py b/setup.py index 00c2a40307..35b1a2c8a3 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,9 @@ import ast import subprocess import sys +import copy +from collections import OrderedDict + # NOTE: have to programmatically include third-party dependencies in `setup.py`. def installPackage(package: str): @@ -41,6 +44,8 @@ def installPackage(package: str): PHASE0 = 'phase0' ALTAIR = 'altair' BELLATRIX = 'bellatrix' +CAPELLA = 'capella' + # The helper functions that are used when defining constants CONSTANT_DEP_SUNDRY_CONSTANTS_FUNCTIONS = ''' @@ -549,9 +554,22 @@ def hardcoded_custom_type_dep_constants(cls) -> str: return {**super().hardcoded_custom_type_dep_constants(), **constants} +# +# CapellaSpecBuilder +# +class CapellaSpecBuilder(BellatrixSpecBuilder): + fork: str = CAPELLA + + @classmethod + def imports(cls, preset_name: str): + return super().imports(preset_name) + f''' +from eth2spec.bellatrix import {preset_name} as bellatrix +''' + + spec_builders = { builder.fork: builder - for builder in (Phase0SpecBuilder, AltairSpecBuilder, BellatrixSpecBuilder) + for builder in (Phase0SpecBuilder, AltairSpecBuilder, BellatrixSpecBuilder, CapellaSpecBuilder) } @@ -684,7 +702,7 @@ def combine_dicts(old_dict: Dict[str, T], new_dict: Dict[str, T]) -> Dict[str, T 'uint8', 'uint16', 'uint32', 'uint64', 'uint128', 'uint256', 'bytes', 'byte', 'ByteList', 'ByteVector', 'Dict', 'dict', 'field', 'ceillog2', 'floorlog2', 'Set', - 'Optional', + 'Optional', 'Sequence', ] @@ -710,7 +728,6 @@ def dependency_order_class_objects(objects: Dict[str, str], custom_types: Dict[s for item in [dep, key] + key_list[key_list.index(dep)+1:]: objects[item] = objects.pop(item) - def combine_ssz_objects(old_objects: Dict[str, str], new_objects: Dict[str, str], custom_types) -> Dict[str, str]: """ Takes in old spec and new spec ssz objects, combines them, @@ -800,7 +817,12 @@ def _build_spec(preset_name: str, fork: str, spec_object = combine_spec_objects(spec_object, value) class_objects = {**spec_object.ssz_objects, **spec_object.dataclasses} - dependency_order_class_objects(class_objects, spec_object.custom_types) + + # Ensure it's ordered after multiple forks + new_objects = {} + while OrderedDict(new_objects) != OrderedDict(class_objects): + new_objects = copy.deepcopy(class_objects) + dependency_order_class_objects(class_objects, spec_object.custom_types) return objects_to_spec(preset_name, spec_object, spec_builders[fork], class_objects) @@ -847,14 +869,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): + if self.spec_fork in (PHASE0, ALTAIR, BELLATRIX, CAPELLA): 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): + if self.spec_fork in (ALTAIR, BELLATRIX, CAPELLA): self.md_doc_paths += """ specs/altair/beacon-chain.md specs/altair/bls.md @@ -863,7 +885,7 @@ def finalize_options(self): specs/altair/p2p-interface.md specs/altair/sync-protocol.md """ - if self.spec_fork == BELLATRIX: + if self.spec_fork in (BELLATRIX, CAPELLA): self.md_doc_paths += """ specs/bellatrix/beacon-chain.md specs/bellatrix/fork.md @@ -871,6 +893,14 @@ def finalize_options(self): specs/bellatrix/validator.md sync/optimistic.md """ + if self.spec_fork == CAPELLA: + self.md_doc_paths += """ + specs/capella/beacon-chain.md + specs/capella/fork.md + specs/capella/fork-choice.md + specs/capella/validator.md + specs/capella/p2p-interface.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/capella/beacon-chain.md b/specs/capella/beacon-chain.md new file mode 100644 index 0000000000..81a7c17072 --- /dev/null +++ b/specs/capella/beacon-chain.md @@ -0,0 +1,326 @@ +# Capella -- The Beacon Chain + +## Table of contents + + + + + +- [Introduction](#introduction) +- [Custom types](#custom-types) +- [Constants](#constants) +- [Preset](#preset) + - [State list lengths](#state-list-lengths) + - [Execution](#execution) +- [Configuration](#configuration) +- [Containers](#containers) + - [New containers](#new-containers) + - [`Withdrawal`](#withdrawal) + - [Extended Containers](#extended-containers) + - [`ExecutionPayload`](#executionpayload) + - [`ExecutionPayloadHeader`](#executionpayloadheader) + - [`Validator`](#validator) + - [`BeaconState`](#beaconstate) +- [Helpers](#helpers) + - [Beacon state mutators](#beacon-state-mutators) + - [`withdraw`](#withdraw) + - [Predicates](#predicates) + - [`is_fully_withdrawable_validator`](#is_fully_withdrawable_validator) +- [Beacon chain state transition function](#beacon-chain-state-transition-function) + - [Epoch processing](#epoch-processing) + - [Withdrawals](#withdrawals) + - [Block processing](#block-processing) + - [New `process_withdrawals`](#new-process_withdrawals) + - [Modified `process_execution_payload`](#modified-process_execution_payload) + + + + +## Introduction + +Capella is a consensus-layer upgrade containing a number of features related +to validator withdrawals. Including: +* Automatic withdrawals of `withdrawable` validators +* Partial withdrawals during block proposal +* Operation to change from `BLS_WITHDRAWAL_PREFIX` to + `ETH1_ADDRESS_WITHDRAWAL_PREFIX` versioned withdrawal credentials to enable withdrawals for a validator + +## Custom types + +## Constants + +We define the following Python custom types for type hinting and readability: + +| Name | SSZ equivalent | Description | +| - | - | - | +| `WithdrawalIndex` | `uint64` | an index of a `Withdrawal`| + +## Preset + +### State list lengths + +| Name | Value | Unit | Duration | +| - | - | :-: | :-: | +| `WITHDRAWALS_QUEUE_LIMIT` | `uint64(2**40)` (= 1,099,511,627,776) | withdrawals enqueued in state| + +### Execution + +| Name | Value | Description | +| - | - | - | +| `MAX_WITHDRAWALS_PER_PAYLOAD` | `uint64(2**4)` (= 16) | Maximum amount of withdrawals allowed in each payload | + +## Configuration + +## Containers + +### New containers + +#### `Withdrawal` + +```python +class Withdrawal(Container): + index: WithdrawalIndex + address: ExecutionAddress + amount: Gwei +``` + +### Extended Containers + +#### `ExecutionPayload` + +```python +class ExecutionPayload(Container): + # Execution block header fields + parent_hash: Hash32 + fee_recipient: ExecutionAddress # 'beneficiary' in the yellow paper + state_root: Bytes32 + receipts_root: Bytes32 + logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] + prev_randao: Bytes32 # 'difficulty' in the yellow paper + block_number: uint64 # 'number' in the yellow paper + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + # Extra payload fields + block_hash: Hash32 # Hash of execution block + transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD] + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] # [New in Capella] +``` + +#### `ExecutionPayloadHeader` + +```python +class ExecutionPayloadHeader(Container): + # Execution block header fields + parent_hash: Hash32 + fee_recipient: ExecutionAddress + state_root: Bytes32 + receipts_root: Bytes32 + logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] + prev_randao: Bytes32 + block_number: uint64 + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + # Extra payload fields + block_hash: Hash32 # Hash of execution block + transactions_root: Root + withdrawals_root: Root # [New in Capella] +``` + +#### `Validator` + +```python +class Validator(Container): + pubkey: BLSPubkey + withdrawal_credentials: Bytes32 # Commitment to pubkey for withdrawals + effective_balance: Gwei # Balance at stake + slashed: boolean + # Status epochs + activation_eligibility_epoch: Epoch # When criteria for activation were met + activation_epoch: Epoch + exit_epoch: Epoch + withdrawable_epoch: Epoch # When validator can withdraw funds + fully_withdrawn_epoch: Epoch # [New in Capella] +``` + +#### `BeaconState` + +```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 + withdrawal_index: WithdrawalIndex + withdrawals_queue: List[Withdrawal, WITHDRAWALS_QUEUE_LIMIT] # [New in Capella] +``` + +## Helpers + +### Beacon state mutators + +#### `withdraw` + +```python +def withdraw_balance(state: BeaconState, index: ValidatorIndex, amount: Gwei) -> None: + # Decrease the validator's balance + decrease_balance(state, index, amount) + # Create a corresponding withdrawal receipt + withdrawal = Withdrawal( + index=state.withdrawal_index, + address=state.validators[index].withdrawal_credentials[12:], + amount=amount, + ) + state.withdrawal_index = WithdrawalIndex(state.withdrawal_index + 1) + state.withdrawals_queue.append(withdrawal) +``` + +### Predicates + +#### `is_fully_withdrawable_validator` + +```python +def is_fully_withdrawable_validator(validator: Validator, epoch: Epoch) -> bool: + """ + Check if ``validator`` is fully withdrawable. + """ + is_eth1_withdrawal_prefix = validator.withdrawal_credentials[:1] == ETH1_ADDRESS_WITHDRAWAL_PREFIX + return is_eth1_withdrawal_prefix and validator.withdrawable_epoch <= epoch < validator.fully_withdrawn_epoch +``` + +## Beacon chain state transition function + +### Epoch processing + +```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_effective_balance_updates(state) + process_slashings_reset(state) + process_randao_mixes_reset(state) + process_historical_roots_update(state) + process_participation_flag_updates(state) + process_sync_committee_updates(state) + process_full_withdrawals(state) # [New in Capella] +``` + +#### Withdrawals + +*Note*: The function `process_full_withdrawals` is new. + +```python +def process_full_withdrawals(state: BeaconState) -> None: + current_epoch = get_current_epoch(state) + for index, validator in enumerate(state.validators): + if is_fully_withdrawable_validator(validator, current_epoch): + # TODO, consider the zero-balance case + withdraw_balance(state, ValidatorIndex(index), state.balances[index]) + validator.fully_withdrawn_epoch = current_epoch +``` + +### Block processing + +```python +def process_block(state: BeaconState, block: BeaconBlock) -> None: + process_block_header(state, block) + if is_execution_enabled(state, block.body): + process_withdrawals(state, block.body.execution_payload) # [New in Capella] + process_execution_payload(state, block.body.execution_payload, EXECUTION_ENGINE) # [Modified in Capella] + process_randao(state, block.body) + process_eth1_data(state, block.body) + process_operations(state, block.body) + process_sync_aggregate(state, block.body.sync_aggregate) +``` + +#### New `process_withdrawals` + +```python +def process_withdrawals(state: BeaconState, payload: ExecutionPayload) -> None: + num_withdrawals = min(MAX_WITHDRAWALS_PER_PAYLOAD, len(state.withdrawals_queue)) + dequeued_withdrawals = state.withdrawals_queue[:num_withdrawals] + + assert len(dequeued_withdrawals) == len(payload.withdrawals) + for dequeued_withdrawal, withdrawal in zip(dequeued_withdrawals, payload.withdrawals): + assert dequeued_withdrawal == withdrawal + + # Remove dequeued withdrawals from state + state.withdrawals_queue = state.withdrawals_queue[num_withdrawals:] +``` + +#### Modified `process_execution_payload` + +*Note*: The function `process_execution_payload` is modified to use the new `ExecutionPayloadHeader` type. + +```python +def process_execution_payload(state: BeaconState, payload: ExecutionPayload, execution_engine: ExecutionEngine) -> None: + # Verify consistency of the parent hash with respect to the previous execution payload header + if is_merge_transition_complete(state): + assert payload.parent_hash == state.latest_execution_payload_header.block_hash + # Verify prev_randao + assert payload.prev_randao == get_randao_mix(state, get_current_epoch(state)) + # Verify timestamp + assert payload.timestamp == compute_timestamp_at_slot(state, state.slot) + # Verify the execution payload is valid + assert execution_engine.notify_new_payload(payload) + # Cache execution payload header + state.latest_execution_payload_header = ExecutionPayloadHeader( + parent_hash=payload.parent_hash, + fee_recipient=payload.fee_recipient, + state_root=payload.state_root, + receipts_root=payload.receipts_root, + logs_bloom=payload.logs_bloom, + prev_randao=payload.prev_randao, + block_number=payload.block_number, + gas_limit=payload.gas_limit, + gas_used=payload.gas_used, + timestamp=payload.timestamp, + extra_data=payload.extra_data, + base_fee_per_gas=payload.base_fee_per_gas, + block_hash=payload.block_hash, + transactions_root=hash_tree_root(payload.transactions), + withdrawals_root=hash_tree_root(payload.withdrawals), # [New in Capella] + ) +``` diff --git a/specs/capella/fork-choice.md b/specs/capella/fork-choice.md new file mode 100644 index 0000000000..f7a76275d4 --- /dev/null +++ b/specs/capella/fork-choice.md @@ -0,0 +1,62 @@ +# Capella -- Fork Choice + +**Notice**: This document is a work-in-progress for researchers and implementers. + +## Table of contents + + + + +- [Introduction](#introduction) +- [Custom types](#custom-types) +- [Protocols](#protocols) + - [`ExecutionEngine`](#executionengine) + - [`notify_forkchoice_updated`](#notify_forkchoice_updated) +- [Helpers](#helpers) + - [Extended `PayloadAttributes`](#extended-payloadattributes) + + + + +## Introduction + +This is the modification of the fork choice according to the Capella upgrade. + +Unless stated explicitly, all prior functionality from [Bellatrix](../bellatrix/fork-choice.md) is inherited. + +## Custom types + +## Protocols + +### `ExecutionEngine` + +*Note*: The `notify_forkchoice_updated` function is modified in the `ExecutionEngine` protocol at the Capella upgrade. + +#### `notify_forkchoice_updated` + +The only change made is to the `PayloadAttributes` container through the addition of `withdrawals`. +Otherwise, `notify_forkchoice_updated` inherits all prior functionality. + +```python +def notify_forkchoice_updated(self: ExecutionEngine, + head_block_hash: Hash32, + safe_block_hash: Hash32, + finalized_block_hash: Hash32, + payload_attributes: Optional[PayloadAttributes]) -> Optional[PayloadId]: + ... +``` + +## Helpers + +### Extended `PayloadAttributes` + +`PayloadAttributes` is extended with the `withdrawals` field. + +```python +@dataclass +class PayloadAttributes(object): + timestamp: uint64 + prev_randao: Bytes32 + suggested_fee_recipient: ExecutionAddress + withdrawals: Sequence[Withdrawal] # new in Capella +``` diff --git a/specs/capella/fork.md b/specs/capella/fork.md new file mode 100644 index 0000000000..5f015a4ff2 --- /dev/null +++ b/specs/capella/fork.md @@ -0,0 +1,111 @@ +# Capella -- Fork Logic + +## Table of contents + + + + +- [Introduction](#introduction) +- [Configuration](#configuration) +- [Fork to Capella](#fork-to-capella) + - [Fork trigger](#fork-trigger) + - [Upgrading the state](#upgrading-the-state) + + + +## Introduction + +This document describes the process of the Capella upgrade. + +## Configuration + +Warning: this configuration is not definitive. + +| Name | Value | +| - | - | +| `CAPELLA_FORK_VERSION` | `Version('0x03000000')` | +| `CAPELLA_FORK_EPOCH` | `Epoch(18446744073709551615)` **TBD** | + + +## Fork to Capella + +### Fork trigger + +The fork is triggered at epoch `CAPELLA_FORK_EPOCH`. + +Note that for the pure Capella networks, we don't apply `upgrade_to_capella` since it starts with Capella version logic. + +### Upgrading the state + +If `state.slot % SLOTS_PER_EPOCH == 0` and `compute_epoch_at_slot(state.slot) == CAPELLA_FORK_EPOCH`, +an irregular state change is made to upgrade to Capella. + +The upgrade occurs after the completion of the inner loop of `process_slots` that sets `state.slot` equal to `CAPELLA_FORK_EPOCH * SLOTS_PER_EPOCH`. +Care must be taken when transitioning through the fork boundary as implementations will need a modified [state transition function](../phase0/beacon-chain.md#beacon-chain-state-transition-function) that deviates from the Phase 0 document. +In particular, the outer `state_transition` function defined in the Phase 0 document will not expose the precise fork slot to execute the upgrade in the presence of skipped slots at the fork boundary. Instead the logic must be within `process_slots`. + +```python +def upgrade_to_capella(pre: bellatrix.BeaconState) -> BeaconState: + epoch = bellatrix.get_current_epoch(pre) + 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=CAPELLA_FORK_VERSION, + epoch=epoch, + ), + # 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=[], + balances=pre.balances, + # 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 + withdrawal_index=WithdrawalIndex(0), + withdrawals_queue=[], + ) + + for pre_validator in pre.validators: + post_validator = Validator( + pubkey=pre_validator.pubkey, + withdrawal_credentials=pre_validator.withdrawal_credentials, + effective_balance=pre_validator.effective_balance, + slashed=pre_validator.slashed, + activation_eligibility_epoch=pre_validator.activation_eligibility_epoch, + activation_epoch=pre_validator.activation_epoch, + exit_epoch=pre_validator.exit_epoch, + withdrawable_epoch=pre_validator.withdrawable_epoch, + fully_withdrawn_epoch=FAR_FUTURE_EPOCH, + ) + post.validators.append(post_validator) + + return post +``` diff --git a/specs/capella/p2p-interface.md b/specs/capella/p2p-interface.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/specs/capella/validator.md b/specs/capella/validator.md new file mode 100644 index 0000000000..3caeaec9c0 --- /dev/null +++ b/specs/capella/validator.md @@ -0,0 +1,109 @@ +# Capella -- Honest Validator + +**Notice**: This document is a work-in-progress for researchers and implementers. + +## Table of contents + + + + + +- [Introduction](#introduction) +- [Prerequisites](#prerequisites) +- [Helpers](#helpers) +- [Protocols](#protocols) + - [`ExecutionEngine`](#executionengine) + - [`get_payload`](#get_payload) +- [Beacon chain responsibilities](#beacon-chain-responsibilities) + - [Block proposal](#block-proposal) + - [Constructing the `BeaconBlockBody`](#constructing-the-beaconblockbody) + - [ExecutionPayload](#executionpayload) + + + + +## Introduction + +This document represents the changes to be made in the code of an "honest validator" to implement the Capella upgrade. + +## Prerequisites + +This document is an extension of the [Bellatrix -- Honest Validator](../bellatrix/validator.md) guide. +All behaviors and definitions defined in this document, and documents it extends, carry over unless explicitly noted or overridden. + +All terminology, constants, functions, and protocol mechanics defined in the updated Beacon Chain doc of [Capella](./beacon-chain.md) are requisite for this document and used throughout. +Please see related Beacon Chain doc before continuing and use them as a reference throughout. + +## Helpers + +## Protocols + +### `ExecutionEngine` + +#### `get_payload` + +`get_payload` returns the upgraded Capella `ExecutionPayload` type. + +## Beacon chain responsibilities + +All validator responsibilities remain unchanged other than those noted below. + +### Block proposal + +#### Constructing the `BeaconBlockBody` + +##### ExecutionPayload + +`ExecutionPayload`s are constructed as they were in Bellatrix, except that the +expected withdrawals for the slot must be gathered from the `state` (utilizing the +helper `get_expected_withdrawals`) and passed into the `ExecutionEngine` within `prepare_execution_payload`. + + +```python +def get_expected_withdrawals(state: BeaconState) -> Sequence[Withdrawal]: + num_withdrawals = min(MAX_WITHDRAWALS_PER_PAYLOAD, len(state.withdrawals_queue)) + return state.withdrawals_queue[:num_withdrawals] +``` + +*Note*: The only change made to `prepare_execution_payload` is to call +`get_expected_withdrawals()` to set the new `withdrawals` field of `PayloadAttributes`. + +```python +def prepare_execution_payload(state: BeaconState, + pow_chain: Dict[Hash32, PowBlock], + finalized_block_hash: Hash32, + suggested_fee_recipient: ExecutionAddress, + execution_engine: ExecutionEngine) -> Optional[PayloadId]: + if not is_merge_transition_complete(state): + is_terminal_block_hash_set = TERMINAL_BLOCK_HASH != Hash32() + is_activation_epoch_reached = get_current_epoch(state) >= TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH + if is_terminal_block_hash_set and not is_activation_epoch_reached: + # Terminal block hash is set but activation epoch is not yet reached, no prepare payload call is needed + return None + + terminal_pow_block = get_terminal_pow_block(pow_chain) + if terminal_pow_block is None: + # Pre-merge, no prepare payload call is needed + return None + # Signify merge via producing on top of the terminal PoW block + parent_hash = terminal_pow_block.block_hash + else: + # Post-merge, normal payload + parent_hash = state.latest_execution_payload_header.block_hash + + # Set the forkchoice head and initiate the payload build process + payload_attributes = PayloadAttributes( + timestamp=compute_timestamp_at_slot(state, state.slot), + prev_randao=get_randao_mix(state, get_current_epoch(state)), + suggested_fee_recipient=suggested_fee_recipient, + withdrawals=get_expected_withdrawals(state), # [New in Capella] + ) + # Set safe and head block hashes to the same value + return execution_engine.notify_forkchoice_updated( + head_block_hash=parent_hash, + # TODO: Use `parent_hash` as a stub for now. + safe_block_hash=parent_hash, + finalized_block_hash=finalized_block_hash, + payload_attributes=payload_attributes, + ) +``` diff --git a/tests/core/pyspec/eth2spec/test/capella/block_processing/test_process_withdrawals.py b/tests/core/pyspec/eth2spec/test/capella/block_processing/test_process_withdrawals.py new file mode 100644 index 0000000000..204816c994 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/capella/block_processing/test_process_withdrawals.py @@ -0,0 +1,242 @@ +from eth2spec.test.helpers.execution_payload import ( + build_empty_execution_payload, +) + +from eth2spec.test.context import spec_state_test, expect_assertion_error, with_capella_and_later + +from eth2spec.test.helpers.state import next_slot + + +def prepare_withdrawals_queue(spec, state, num_withdrawals): + pre_queue_len = len(state.withdrawals_queue) + + for i in range(num_withdrawals): + withdrawal = spec.Withdrawal( + index=i + 5, + address=b'\x42' * 20, + amount=200000 + i, + ) + state.withdrawals_queue.append(withdrawal) + + assert len(state.withdrawals_queue) == num_withdrawals + pre_queue_len + + +def run_withdrawals_processing(spec, state, execution_payload, valid=True): + """ + Run ``process_execution_payload``, yielding: + - pre-state ('pre') + - execution payload ('execution_payload') + - post-state ('post'). + If ``valid == False``, run expecting ``AssertionError`` + """ + + pre_withdrawals_queue = state.withdrawals_queue.copy() + num_withdrawals = min(spec.MAX_WITHDRAWALS_PER_PAYLOAD, len(pre_withdrawals_queue)) + + yield 'pre', state + yield 'execution_payload', execution_payload + + if not valid: + expect_assertion_error(lambda: spec.process_withdrawals(state, execution_payload)) + yield 'post', None + return + + spec.process_withdrawals(state, execution_payload) + + yield 'post', state + + if len(pre_withdrawals_queue) == 0: + assert len(state.withdrawals_queue) == 0 + elif len(pre_withdrawals_queue) <= num_withdrawals: + assert len(state.withdrawals_queue) == 0 + else: + assert state.withdrawals_queue == pre_withdrawals_queue[num_withdrawals:] + + +@with_capella_and_later +@spec_state_test +def test_success_empty_queue(spec, state): + assert len(state.withdrawals_queue) == 0 + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing(spec, state, execution_payload) + + +@with_capella_and_later +@spec_state_test +def test_success_one_in_queue(spec, state): + prepare_withdrawals_queue(spec, state, 1) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing(spec, state, execution_payload) + + +@with_capella_and_later +@spec_state_test +def test_success_max_per_slot_in_queue(spec, state): + prepare_withdrawals_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing(spec, state, execution_payload) + + +@with_capella_and_later +@spec_state_test +def test_success_a_lot_in_queue(spec, state): + prepare_withdrawals_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + + yield from run_withdrawals_processing(spec, state, execution_payload) + + +# +# Failure cases in which the number of withdrawals in the execution_payload is incorrect +# + +@with_capella_and_later +@spec_state_test +def test_fail_empty_queue_non_empty_withdrawals(spec, state): + assert len(state.withdrawals_queue) == 0 + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + withdrawal = spec.Withdrawal( + index=0, + address=b'\x30' * 20, + amount=420, + ) + execution_payload.withdrawals.append(withdrawal) + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_one_in_queue_none_in_withdrawals(spec, state): + prepare_withdrawals_queue(spec, state, 1) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.withdrawals = [] + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_one_in_queue_two_in_withdrawals(spec, state): + prepare_withdrawals_queue(spec, state, 1) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.withdrawals.append(execution_payload.withdrawals[0].copy()) + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_max_per_slot_in_queue_one_less_in_withdrawals(spec, state): + prepare_withdrawals_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.withdrawals = execution_payload.withdrawals[:-1] + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_a_lot_in_queue_too_few_in_withdrawals(spec, state): + prepare_withdrawals_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.withdrawals = execution_payload.withdrawals[:-1] + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +# +# Failure cases in which the withdrawals in the execution_payload are incorrect +# + +@with_capella_and_later +@spec_state_test +def test_fail_incorrect_dequeue_index(spec, state): + prepare_withdrawals_queue(spec, state, 1) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.withdrawals[0].index += 1 + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_incorrect_dequeue_address(spec, state): + prepare_withdrawals_queue(spec, state, 1) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.withdrawals[0].address = b'\xff' * 20 + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_incorrect_dequeue_amount(spec, state): + prepare_withdrawals_queue(spec, state, 1) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + execution_payload.withdrawals[0].amount += 1 + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_one_of_many_dequeued_incorrectly(spec, state): + prepare_withdrawals_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + num_withdrawals = len(execution_payload.withdrawals) + + # Pick withdrawal in middle of list and mutate + withdrawal = execution_payload.withdrawals[num_withdrawals // 2] + withdrawal.index += 1 + withdrawal.address = b'\x99' * 20 + withdrawal.amount += 4000000 + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_many_dequeued_incorrectly(spec, state): + prepare_withdrawals_queue(spec, state, spec.MAX_WITHDRAWALS_PER_PAYLOAD * 4) + + next_slot(spec, state) + execution_payload = build_empty_execution_payload(spec, state) + for i, withdrawal in enumerate(execution_payload.withdrawals): + if i % 3 == 0: + withdrawal.index += 1 + elif i % 3 == 1: + withdrawal.address = (i).to_bytes(20, 'big') + else: + withdrawal.amount += 1 + + yield from run_withdrawals_processing(spec, state, execution_payload, valid=False) diff --git a/tests/core/pyspec/eth2spec/test/capella/epoch_processing/test_process_full_withdrawals.py b/tests/core/pyspec/eth2spec/test/capella/epoch_processing/test_process_full_withdrawals.py new file mode 100644 index 0000000000..305f6e1baa --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/capella/epoch_processing/test_process_full_withdrawals.py @@ -0,0 +1,91 @@ +from eth2spec.test.context import ( + with_capella_and_later, + spec_state_test, +) +from eth2spec.test.helpers.epoch_processing import run_epoch_processing_with + + +def set_validator_withdrawable(spec, state, index, withdrawable_epoch=None): + if withdrawable_epoch is None: + withdrawable_epoch = spec.get_current_epoch(state) + + validator = state.validators[index] + validator.withdrawable_epoch = withdrawable_epoch + validator.withdrawal_credentials = spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX + validator.withdrawal_credentials[1:] + + assert spec.is_fully_withdrawable_validator(validator, withdrawable_epoch) + + +def run_process_full_withdrawals(spec, state, num_expected_withdrawals=None): + pre_withdrawal_index = state.withdrawal_index + pre_withdrawals_queue = state.withdrawals_queue + to_be_withdrawn_indices = [ + index for index, validator in enumerate(state.validators) + if spec.is_fully_withdrawable_validator(validator, spec.get_current_epoch(state)) + ] + + if num_expected_withdrawals is not None: + assert len(to_be_withdrawn_indices) == num_expected_withdrawals + + yield from run_epoch_processing_with(spec, state, 'process_full_withdrawals') + + for index in to_be_withdrawn_indices: + validator = state.validators[index] + assert validator.fully_withdrawn_epoch == spec.get_current_epoch(state) + assert state.balances[index] == 0 + + assert len(state.withdrawals_queue) == len(pre_withdrawals_queue) + num_expected_withdrawals + assert state.withdrawal_index == pre_withdrawal_index + num_expected_withdrawals + + +@with_capella_and_later +@spec_state_test +def test_no_withdrawals(spec, state): + pre_validators = state.validators.copy() + yield from run_process_full_withdrawals(spec, state, 0) + + assert pre_validators == state.validators + + +@with_capella_and_later +@spec_state_test +def test_no_withdrawals_but_some_next_epoch(spec, state): + current_epoch = spec.get_current_epoch(state) + + # Make a few validators withdrawable at the *next* epoch + for index in range(3): + set_validator_withdrawable(spec, state, index, current_epoch + 1) + + yield from run_process_full_withdrawals(spec, state, 0) + + +@with_capella_and_later +@spec_state_test +def test_single_withdrawal(spec, state): + # Make one validator withdrawable + set_validator_withdrawable(spec, state, 0) + + assert state.withdrawal_index == 0 + yield from run_process_full_withdrawals(spec, state, 1) + + assert state.withdrawal_index == 1 + + +@with_capella_and_later +@spec_state_test +def test_multi_withdrawal(spec, state): + # Make a few validators withdrawable + for index in range(3): + set_validator_withdrawable(spec, state, index) + + yield from run_process_full_withdrawals(spec, state, 3) + + +@with_capella_and_later +@spec_state_test +def test_all_withdrawal(spec, state): + # Make all validators withdrawable + for index in range(len(state.validators)): + set_validator_withdrawable(spec, state, index) + + yield from run_process_full_withdrawals(spec, state, len(state.validators)) diff --git a/tests/core/pyspec/eth2spec/test/capella/fork/test_capella_fork_basic.py b/tests/core/pyspec/eth2spec/test/capella/fork/test_capella_fork_basic.py new file mode 100644 index 0000000000..a235c9e8a1 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/capella/fork/test_capella_fork_basic.py @@ -0,0 +1,82 @@ +from eth2spec.test.context import ( + with_phases, + with_custom_state, + with_presets, + spec_test, with_state, + low_balances, misc_balances, large_validator_set, +) +from eth2spec.test.utils import with_meta_tags +from eth2spec.test.helpers.constants import ( + BELLATRIX, CAPELLA, + MINIMAL, +) +from eth2spec.test.helpers.state import ( + next_epoch, + next_epoch_via_block, +) +from eth2spec.test.helpers.capella.fork import ( + CAPELLA_FORK_TEST_META_TAGS, + run_fork_test, +) + + +@with_phases(phases=[BELLATRIX], other_phases=[CAPELLA]) +@spec_test +@with_state +@with_meta_tags(CAPELLA_FORK_TEST_META_TAGS) +def test_fork_base_state(spec, phases, state): + yield from run_fork_test(phases[CAPELLA], state) + + +@with_phases(phases=[BELLATRIX], other_phases=[CAPELLA]) +@spec_test +@with_state +@with_meta_tags(CAPELLA_FORK_TEST_META_TAGS) +def test_fork_next_epoch(spec, phases, state): + next_epoch(spec, state) + yield from run_fork_test(phases[CAPELLA], state) + + +@with_phases(phases=[BELLATRIX], other_phases=[CAPELLA]) +@spec_test +@with_state +@with_meta_tags(CAPELLA_FORK_TEST_META_TAGS) +def test_fork_next_epoch_with_block(spec, phases, state): + next_epoch_via_block(spec, state) + yield from run_fork_test(phases[CAPELLA], state) + + +@with_phases(phases=[BELLATRIX], other_phases=[CAPELLA]) +@spec_test +@with_state +@with_meta_tags(CAPELLA_FORK_TEST_META_TAGS) +def test_fork_many_next_epoch(spec, phases, state): + for _ in range(3): + next_epoch(spec, state) + yield from run_fork_test(phases[CAPELLA], state) + + +@with_phases(phases=[BELLATRIX], other_phases=[CAPELLA]) +@with_custom_state(balances_fn=low_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE) +@spec_test +@with_meta_tags(CAPELLA_FORK_TEST_META_TAGS) +def test_fork_random_low_balances(spec, phases, state): + yield from run_fork_test(phases[CAPELLA], state) + + +@with_phases(phases=[BELLATRIX], other_phases=[CAPELLA]) +@with_custom_state(balances_fn=misc_balances, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE) +@spec_test +@with_meta_tags(CAPELLA_FORK_TEST_META_TAGS) +def test_fork_random_misc_balances(spec, phases, state): + yield from run_fork_test(phases[CAPELLA], state) + + +@with_phases(phases=[BELLATRIX], other_phases=[CAPELLA]) +@with_presets([MINIMAL], + reason="mainnet config leads to larger validator set than limit of public/private keys pre-generated") +@with_custom_state(balances_fn=large_validator_set, threshold_fn=lambda spec: spec.config.EJECTION_BALANCE) +@spec_test +@with_meta_tags(CAPELLA_FORK_TEST_META_TAGS) +def test_fork_random_large_validator_set(spec, phases, state): + yield from run_fork_test(phases[CAPELLA], state) diff --git a/tests/core/pyspec/eth2spec/test/capella/fork/test_capella_fork_random.py b/tests/core/pyspec/eth2spec/test/capella/fork/test_capella_fork_random.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 87ae2fc5c2..35e4f1bcb8 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -6,12 +6,14 @@ from eth2spec.phase0 import mainnet as spec_phase0_mainnet, minimal as spec_phase0_minimal from eth2spec.altair import mainnet as spec_altair_mainnet, minimal as spec_altair_minimal from eth2spec.bellatrix import mainnet as spec_bellatrix_mainnet, minimal as spec_bellatrix_minimal +from eth2spec.capella import mainnet as spec_capella_mainnet, minimal as spec_capella_minimal from eth2spec.utils import bls from .exceptions import SkippedTest from .helpers.constants import ( - PHASE0, ALTAIR, BELLATRIX, MINIMAL, MAINNET, - ALL_PHASES, FORKS_BEFORE_ALTAIR, FORKS_BEFORE_BELLATRIX, + PHASE0, ALTAIR, BELLATRIX, CAPELLA, + MINIMAL, MAINNET, + ALL_PHASES, FORKS_BEFORE_ALTAIR, FORKS_BEFORE_BELLATRIX, FORKS_BEFORE_CAPELLA, ALL_FORK_UPGRADES, ) from .helpers.typing import SpecForkName, PresetBaseName @@ -57,6 +59,10 @@ class SpecBellatrix(Spec): ... +class SpecCapella(Spec): + ... + + @dataclass(frozen=True) class ForkMeta: pre_fork_name: str @@ -69,11 +75,13 @@ class ForkMeta: PHASE0: spec_phase0_minimal, ALTAIR: spec_altair_minimal, BELLATRIX: spec_bellatrix_minimal, + CAPELLA: spec_capella_minimal, }, MAINNET: { PHASE0: spec_phase0_mainnet, ALTAIR: spec_altair_mainnet, BELLATRIX: spec_bellatrix_mainnet, + CAPELLA: spec_capella_mainnet, }, } @@ -82,6 +90,7 @@ class SpecForks(TypedDict, total=False): PHASE0: SpecPhase0 ALTAIR: SpecAltair BELLATRIX: SpecBellatrix + CAPELLA: SpecCapella def _prepare_state(balances_fn: Callable[[Any], Sequence[int]], threshold_fn: Callable[[Any], int], @@ -533,8 +542,13 @@ def is_post_bellatrix(spec): return spec.fork not in FORKS_BEFORE_BELLATRIX +def is_post_capella(spec): + return spec.fork not in FORKS_BEFORE_CAPELLA + + with_altair_and_later = with_all_phases_except([PHASE0]) with_bellatrix_and_later = with_all_phases_except([PHASE0, ALTAIR]) +with_capella_and_later = with_all_phases_except([PHASE0, ALTAIR, BELLATRIX]) def only_generator(reason): diff --git a/tests/core/pyspec/eth2spec/test/helpers/capella/fork.py b/tests/core/pyspec/eth2spec/test/helpers/capella/fork.py new file mode 100644 index 0000000000..41975904fe --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/helpers/capella/fork.py @@ -0,0 +1,59 @@ +CAPELLA_FORK_TEST_META_TAGS = { + 'fork': 'capella', +} + + +def run_fork_test(post_spec, pre_state): + yield 'pre', pre_state + + post_state = post_spec.upgrade_to_capella(pre_state) + + # Stable fields + stable_fields = [ + 'genesis_time', 'genesis_validators_root', 'slot', + # History + 'latest_block_header', 'block_roots', 'state_roots', 'historical_roots', + # Eth1 + 'eth1_data', 'eth1_data_votes', 'eth1_deposit_index', + # Registry + 'balances', + # Randomness + 'randao_mixes', + # Slashings + 'slashings', + # Participation + 'previous_epoch_participation', 'current_epoch_participation', + # Finality + 'justification_bits', 'previous_justified_checkpoint', 'current_justified_checkpoint', 'finalized_checkpoint', + # Inactivity + 'inactivity_scores', + # Sync + 'current_sync_committee', 'next_sync_committee', + # Execution + 'latest_execution_payload_header', + ] + for field in stable_fields: + assert getattr(pre_state, field) == getattr(post_state, field) + + # Modified fields + modified_fields = ['fork', 'validators'] + for field in modified_fields: + assert getattr(pre_state, field) != getattr(post_state, field) + + assert len(pre_state.validators) == len(post_state.validators) + for pre_validator, post_validator in zip(pre_state.validators, post_state.validators): + stable_validator_fields = [ + 'pubkey', 'withdrawal_credentials', + 'effective_balance', + 'slashed', + 'activation_eligibility_epoch', 'activation_epoch', 'exit_epoch', 'withdrawable_epoch', + ] + for field in stable_validator_fields: + assert getattr(pre_validator, field) == getattr(post_validator, field) + assert post_validator.fully_withdrawn_epoch == post_spec.FAR_FUTURE_EPOCH + + assert pre_state.fork.current_version == post_state.fork.previous_version + assert post_state.fork.current_version == post_spec.config.CAPELLA_FORK_VERSION + assert post_state.fork.epoch == post_spec.get_current_epoch(post_state) + + yield 'post', post_state diff --git a/tests/core/pyspec/eth2spec/test/helpers/constants.py b/tests/core/pyspec/eth2spec/test/helpers/constants.py index ddd32b14af..0bc6b2e08d 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/constants.py +++ b/tests/core/pyspec/eth2spec/test/helpers/constants.py @@ -8,6 +8,7 @@ PHASE0 = SpecForkName('phase0') ALTAIR = SpecForkName('altair') BELLATRIX = SpecForkName('bellatrix') +CAPELLA = SpecForkName('capella') # Experimental phases (not included in default "ALL_PHASES"): SHARDING = SpecForkName('sharding') @@ -15,16 +16,18 @@ DAS = SpecForkName('das') # The forks that pytest runs with. -ALL_PHASES = (PHASE0, ALTAIR, BELLATRIX) +ALL_PHASES = (PHASE0, ALTAIR, BELLATRIX, CAPELLA) # The forks that output to the test vectors. TESTGEN_FORKS = (PHASE0, ALTAIR, BELLATRIX) FORKS_BEFORE_ALTAIR = (PHASE0,) FORKS_BEFORE_BELLATRIX = (PHASE0, ALTAIR) +FORKS_BEFORE_CAPELLA = (PHASE0, ALTAIR, BELLATRIX) ALL_FORK_UPGRADES = { # pre_fork_name: post_fork_name PHASE0: ALTAIR, ALTAIR: BELLATRIX, + BELLATRIX: CAPELLA, } ALL_PRE_POST_FORKS = ALL_FORK_UPGRADES.items() AFTER_BELLATRIX_UPGRADES = {key: value for key, value in ALL_FORK_UPGRADES.items() if key not in FORKS_BEFORE_ALTAIR} diff --git a/tests/core/pyspec/eth2spec/test/helpers/epoch_processing.py b/tests/core/pyspec/eth2spec/test/helpers/epoch_processing.py index eed259e819..8c27480c04 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/epoch_processing.py +++ b/tests/core/pyspec/eth2spec/test/helpers/epoch_processing.py @@ -28,6 +28,7 @@ def get_process_calls(spec): 'process_participation_record_updates' ), 'process_sync_committee_updates', # altair + 'process_full_withdrawals', # capella # TODO: add sharding processing functions when spec stabilizes. ] diff --git a/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py b/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py index 14602a2cd6..5e70666324 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py +++ b/tests/core/pyspec/eth2spec/test/helpers/execution_payload.py @@ -1,3 +1,6 @@ +from eth2spec.test.helpers.constants import FORKS_BEFORE_CAPELLA + + def build_empty_execution_payload(spec, state, randao_mix=None): """ Assuming a pre-state of the same slot, build a valid ExecutionPayload without any transactions. @@ -25,6 +28,10 @@ def build_empty_execution_payload(spec, state, randao_mix=None): block_hash=spec.Hash32(), transactions=empty_txs, ) + if spec.fork not in FORKS_BEFORE_CAPELLA: + num_withdrawals = min(spec.MAX_WITHDRAWALS_PER_PAYLOAD, len(state.withdrawals_queue)) + payload.withdrawals = state.withdrawals_queue[:num_withdrawals] + # TODO: real RLP + block hash logic would be nice, requires RLP and keccak256 dependency however. payload.block_hash = spec.Hash32(spec.hash(payload.hash_tree_root() + b"FAKE RLP HASH")) @@ -32,7 +39,7 @@ def build_empty_execution_payload(spec, state, randao_mix=None): def get_execution_payload_header(spec, execution_payload): - return spec.ExecutionPayloadHeader( + payload_header = spec.ExecutionPayloadHeader( parent_hash=execution_payload.parent_hash, fee_recipient=execution_payload.fee_recipient, state_root=execution_payload.state_root, @@ -48,6 +55,9 @@ def get_execution_payload_header(spec, execution_payload): block_hash=execution_payload.block_hash, transactions_root=spec.hash_tree_root(execution_payload.transactions) ) + if spec.fork not in FORKS_BEFORE_CAPELLA: + payload_header.withdrawals_root = spec.hash_tree_root(execution_payload.withdrawals) + return payload_header def build_state_with_incomplete_transition(spec, state): diff --git a/tests/core/pyspec/eth2spec/test/helpers/fork_transition.py b/tests/core/pyspec/eth2spec/test/helpers/fork_transition.py index 01d297bd98..ca248d8a55 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_transition.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_transition.py @@ -12,6 +12,7 @@ from eth2spec.test.helpers.constants import ( ALTAIR, BELLATRIX, + CAPELLA, ) from eth2spec.test.helpers.deposits import ( prepare_state_and_deposit, @@ -147,6 +148,8 @@ def do_fork(state, spec, post_spec, fork_epoch, with_block=True, operation_dict= state = post_spec.upgrade_to_altair(state) elif post_spec.fork == BELLATRIX: state = post_spec.upgrade_to_bellatrix(state) + elif post_spec.fork == CAPELLA: + state = post_spec.upgrade_to_capella(state) assert state.fork.epoch == fork_epoch @@ -156,6 +159,9 @@ def do_fork(state, spec, post_spec, fork_epoch, with_block=True, operation_dict= elif post_spec.fork == BELLATRIX: assert state.fork.previous_version == post_spec.config.ALTAIR_FORK_VERSION assert state.fork.current_version == post_spec.config.BELLATRIX_FORK_VERSION + elif post_spec.fork == CAPELLA: + assert state.fork.previous_version == post_spec.config.BELLATRIX_FORK_VERSION + assert state.fork.current_version == post_spec.config.CAPELLA_FORK_VERSION if with_block: return state, _state_transition_and_sign_block_at_slot(post_spec, state, operation_dict=operation_dict) diff --git a/tests/core/pyspec/eth2spec/test/helpers/genesis.py b/tests/core/pyspec/eth2spec/test/helpers/genesis.py index d0e4708932..1ca408598e 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/genesis.py +++ b/tests/core/pyspec/eth2spec/test/helpers/genesis.py @@ -1,6 +1,6 @@ from eth2spec.test.helpers.constants import ( ALTAIR, BELLATRIX, - FORKS_BEFORE_ALTAIR, FORKS_BEFORE_BELLATRIX, + FORKS_BEFORE_ALTAIR, FORKS_BEFORE_BELLATRIX, FORKS_BEFORE_CAPELLA, ) from eth2spec.test.helpers.keys import pubkeys @@ -9,7 +9,7 @@ def build_mock_validator(spec, i: int, balance: int): pubkey = pubkeys[i] # insecurely use pubkey as withdrawal key as well withdrawal_credentials = spec.BLS_WITHDRAWAL_PREFIX + spec.hash(pubkey)[1:] - return spec.Validator( + validator = spec.Validator( pubkey=pubkeys[i], withdrawal_credentials=withdrawal_credentials, activation_eligibility_epoch=spec.FAR_FUTURE_EPOCH, @@ -19,6 +19,11 @@ def build_mock_validator(spec, i: int, balance: int): effective_balance=min(balance - balance % spec.EFFECTIVE_BALANCE_INCREMENT, spec.MAX_EFFECTIVE_BALANCE) ) + if spec.fork not in FORKS_BEFORE_CAPELLA: + validator.fully_withdrawn_epoch = spec.FAR_FUTURE_EPOCH + + return validator + def get_sample_genesis_execution_payload_header(spec, eth1_block_hash=None): diff --git a/tests/core/pyspec/eth2spec/test/phase0/unittests/fork_choice/test_on_attestation.py b/tests/core/pyspec/eth2spec/test/phase0/unittests/fork_choice/test_on_attestation.py index 656c536a53..4e8d4bbaa7 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/unittests/fork_choice/test_on_attestation.py +++ b/tests/core/pyspec/eth2spec/test/phase0/unittests/fork_choice/test_on_attestation.py @@ -1,7 +1,7 @@ from eth2spec.test.context import with_all_phases, spec_state_test from eth2spec.test.helpers.block import build_empty_block_for_next_slot from eth2spec.test.helpers.attestations import get_valid_attestation, sign_attestation -from eth2spec.test.helpers.constants import PHASE0, ALTAIR, BELLATRIX +from eth2spec.test.helpers.constants import PHASE0, ALTAIR, BELLATRIX, CAPELLA from eth2spec.test.helpers.state import transition_to, state_transition_and_sign_block, next_epoch, next_slot from eth2spec.test.helpers.fork_choice import get_genesis_forkchoice_store @@ -19,7 +19,7 @@ def run_on_attestation(spec, state, store, attestation, valid=True): spec.on_attestation(store, attestation) sample_index = indexed_attestation.attesting_indices[0] - if spec.fork in (PHASE0, ALTAIR, BELLATRIX): + if spec.fork in (PHASE0, ALTAIR, BELLATRIX, CAPELLA): latest_message = spec.LatestMessage( epoch=attestation.data.target.epoch, root=attestation.data.beacon_block_root,