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 ec3302e27b..e3ff8e455c 100644 --- a/Makefile +++ b/Makefile @@ -25,9 +25,11 @@ 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) + $(wildcard $(SPEC_DIR)/sharding/*.md) \ + $(wildcard $(SPEC_DIR)/eip4844/*.md) COV_HTML_OUT=.htmlcov COV_HTML_OUT_DIR=$(PY_SPEC_DIR)/$(COV_HTML_OUT) @@ -67,7 +69,7 @@ partial_clean: clean: partial_clean rm -rf venv - # legacy cleanup. The pyspec venv should be located at the repository root + # legacy cleanup. The pyspec venv should be located at the repository root rm -rf $(PY_SPEC_DIR)/venv rm -rf $(DEPOSIT_CONTRACT_COMPILER_DIR)/venv rm -rf $(DEPOSIT_CONTRACT_TESTER_DIR)/venv @@ -97,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; @@ -135,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..1c0a12d4e9 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 @@ -79,8 +84,8 @@ CHURN_LIMIT_QUOTIENT: 65536 # Fork choice # --------------------------------------------------------------- -# 70% -PROPOSER_SCORE_BOOST: 70 +# 40% +PROPOSER_SCORE_BOOST: 40 # Deposit contract # --------------------------------------------------------------- diff --git a/configs/minimal.yaml b/configs/minimal.yaml index 61deb0be43..e619ee9316 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 @@ -78,8 +81,8 @@ CHURN_LIMIT_QUOTIENT: 32 # Fork choice # --------------------------------------------------------------- -# 70% -PROPOSER_SCORE_BOOST: 70 +# 40% +PROPOSER_SCORE_BOOST: 40 # Deposit contract diff --git a/fork_choice/safe-block.md b/fork_choice/safe-block.md new file mode 100644 index 0000000000..490d245381 --- /dev/null +++ b/fork_choice/safe-block.md @@ -0,0 +1,48 @@ +# Fork Choice -- Safe Block + +## Table of contents + + + + +- [Introduction](#introduction) +- [`get_safe_beacon_block_root`](#get_safe_beacon_block_root) +- [`get_safe_execution_payload_hash`](#get_safe_execution_payload_hash) + + + + +## Introduction + +Under honest majority and certain network synchronicity assumptions +there exist a block that is safe from re-orgs. Normally this block is +pretty close to the head of canonical chain which makes it valuable +to expose a safe block to users. + +This section describes an algorithm to find a safe block. + +## `get_safe_beacon_block_root` + +```python +def get_safe_beacon_block_root(store: Store) -> Root: + # Use most recent justified block as a stopgap + return store.justified_checkpoint.root +``` +*Note*: Currently safe block algorithm simply returns `store.justified_checkpoint.root` +and is meant to be improved in the future. + +## `get_safe_execution_payload_hash` + +```python +def get_safe_execution_payload_hash(store: Store) -> Hash32: + safe_block_root = get_safe_beacon_block_root(store) + safe_block = store.blocks[safe_block_root] + + # Return Hash32() if no payload is yet justified + if compute_epoch_at_slot(safe_block.slot) >= BELLATRIX_FORK_EPOCH: + return safe_block.body.execution_payload.block_hash + else: + return Hash32() +``` + +*Note*: This helper uses beacon block container extended in [Bellatrix](../specs/bellatrix/beacon-chain.md). 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 f826968f21..57d8d8fe12 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 = ''' @@ -529,6 +534,7 @@ def notify_new_payload(self: ExecutionEngine, execution_payload: ExecutionPayloa def notify_forkchoice_updated(self: ExecutionEngine, head_block_hash: Hash32, + safe_block_hash: Hash32, finalized_block_hash: Hash32, payload_attributes: Optional[PayloadAttributes]) -> Optional[PayloadId]: pass @@ -548,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) } @@ -683,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', ] @@ -709,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, @@ -799,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) @@ -846,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 @@ -862,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 @@ -870,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) @@ -1017,7 +1048,7 @@ def run(self): "eth-typing>=2.1.0,<3.0.0", "pycryptodome==3.9.4", "py_ecc==5.2.0", - "milagro_bls_binding==1.6.3", + "milagro_bls_binding==1.9.0", "dataclasses==0.6", "remerkleable==0.1.24", RUAMEL_YAML_VERSION, diff --git a/specs/altair/p2p-interface.md b/specs/altair/p2p-interface.md index f2700b3115..8d6b1c433a 100644 --- a/specs/altair/p2p-interface.md +++ b/specs/altair/p2p-interface.md @@ -144,6 +144,7 @@ def get_sync_subcommittee_pubkeys(state: BeaconState, subcommittee_index: uint64 - _[REJECT]_ `contribution_and_proof.selection_proof` selects the validator as an aggregator for the slot -- i.e. `is_sync_committee_aggregator(contribution_and_proof.selection_proof)` returns `True`. - _[REJECT]_ The aggregator's validator index is in the declared subcommittee of the current sync committee -- i.e. `state.validators[contribution_and_proof.aggregator_index].pubkey in get_sync_subcommittee_pubkeys(state, contribution.subcommittee_index)`. +- _[IGNORE]_ A valid sync committee contribution with equal `slot`, `beacon_block_root` and `subcommittee_index` whose `aggregation_bits` is non-strict superset has _not_ already been seen. - _[IGNORE]_ The sync committee contribution is the first valid contribution received for the aggregator with index `contribution_and_proof.aggregator_index` for the slot `contribution.slot` and subcommittee index `contribution.subcommittee_index` (this requires maintaining a cache of size `SYNC_COMMITTEE_SIZE` for this topic that can be flushed after each slot). diff --git a/specs/altair/sync-protocol.md b/specs/altair/sync-protocol.md index f6809eab5f..8751ba6e8b 100644 --- a/specs/altair/sync-protocol.md +++ b/specs/altair/sync-protocol.md @@ -53,7 +53,7 @@ uses sync committees introduced in [this beacon chain extension](./beacon-chain. | Name | Value | Unit | Duration | | - | - | - | - | | `MIN_SYNC_COMMITTEE_PARTICIPANTS` | `1` | validators | -| `UPDATE_TIMEOUT` | `SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD` | epochs | ~27.3 hours | +| `UPDATE_TIMEOUT` | `SLOTS_PER_EPOCH * EPOCHS_PER_SYNC_COMMITTEE_PERIOD` | slots | ~27.3 hours | ## Containers diff --git a/specs/bellatrix/fork-choice.md b/specs/bellatrix/fork-choice.md index 0f5a7b6463..60a54da9c1 100644 --- a/specs/bellatrix/fork-choice.md +++ b/specs/bellatrix/fork-choice.md @@ -12,6 +12,7 @@ - [Protocols](#protocols) - [`ExecutionEngine`](#executionengine) - [`notify_forkchoice_updated`](#notify_forkchoice_updated) + - [`safe_block_hash`](#safe_block_hash) - [Helpers](#helpers) - [`PayloadAttributes`](#payloadattributes) - [`PowBlock`](#powblock) @@ -47,8 +48,9 @@ The Engine API may be used to implement it with an external execution engine. #### `notify_forkchoice_updated` -This function performs two actions *atomically*: +This function performs three actions *atomically*: * Re-organizes the execution payload chain and corresponding state to make `head_block_hash` the head. +* Updates safe block hash with the value provided by `safe_block_hash` parameter. * Applies finality to the execution state: it irreversibly persists the chain of all execution payloads and corresponding state, up to and including `finalized_block_hash`. @@ -58,18 +60,24 @@ Additionally, if `payload_attributes` is provided, this function sets in motion ```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]: ... ``` -*Note*: The call of the `notify_forkchoice_updated` function maps on the `POS_FORKCHOICE_UPDATED` event defined in the [EIP-3675](https://eips.ethereum.org/EIPS/eip-3675#definitions). +*Note*: The `(head_block_hash, finalized_block_hash)` values of the `notify_forkchoice_updated` function call maps on the `POS_FORKCHOICE_UPDATED` event defined in the [EIP-3675](https://eips.ethereum.org/EIPS/eip-3675#definitions). As per EIP-3675, before a post-transition block is finalized, `notify_forkchoice_updated` MUST be called with `finalized_block_hash = Hash32()`. *Note*: Client software MUST NOT call this function until the transition conditions are met on the PoW network, i.e. there exists a block for which `is_valid_terminal_pow_block` function returns `True`. *Note*: Client software MUST call this function to initiate the payload build process to produce the merge transition block; the `head_block_hash` parameter MUST be set to the hash of a terminal PoW block in this case. +##### `safe_block_hash` + +The `safe_block_hash` parameter MUST be set to return value of +[`get_safe_execution_payload_hash(store: Store)`](../../fork_choice/safe-block.md#get_safe_execution_payload_hash) function. + ## Helpers ### `PayloadAttributes` diff --git a/specs/bellatrix/p2p-interface.md b/specs/bellatrix/p2p-interface.md index 60a9be7746..02d4ef6c9f 100644 --- a/specs/bellatrix/p2p-interface.md +++ b/specs/bellatrix/p2p-interface.md @@ -110,7 +110,7 @@ The following gossip validation from prior specifications MUST NOT be applied if ### Transitioning the gossip See gossip transition details found in the [Altair document](../altair/p2p-interface.md#transitioning-the-gossip) for -details on how to handle transitioning gossip topics for Bellatrix. +details on how to handle transitioning gossip topics for EIP-4844. ## The Req/Resp domain @@ -170,7 +170,7 @@ Per `context = compute_fork_digest(fork_version, genesis_validators_root)`: ### Why was the max gossip message size increased at Bellatrix? With the addition of `ExecutionPayload` to `BeaconBlock`s, there is a dynamic -field -- `transactions` -- which can validly exceed the `GOSSIP_MAX_SIZE` limit (1 MiB) put in place in +field -- `transactions` -- which can validly exceed the `GOSSIP_MAX_SIZE` limit (1 MiB) put in place at Phase 0. At the `GAS_LIMIT` (~30M) currently seen on mainnet in 2021, a single transaction filled entirely with data at a cost of 16 gas per byte can create a valid `ExecutionPayload` of ~2 MiB. Thus we need a size limit to at least account for diff --git a/specs/bellatrix/validator.md b/specs/bellatrix/validator.md index 47de49ba65..c88aa9babe 100644 --- a/specs/bellatrix/validator.md +++ b/specs/bellatrix/validator.md @@ -110,9 +110,10 @@ All validator responsibilities remain unchanged other than those noted below. Na To obtain an execution payload, a block proposer building a block on top of a `state` must take the following actions: -1. Set `payload_id = prepare_execution_payload(state, pow_chain, finalized_block_hash, suggested_fee_recipient, execution_engine)`, where: +1. Set `payload_id = prepare_execution_payload(state, pow_chain, safe_block_hash, finalized_block_hash, suggested_fee_recipient, execution_engine)`, where: * `state` is the state object after applying `process_slots(state, slot)` transition to the resulting state of the parent block processing * `pow_chain` is a `Dict[Hash32, PowBlock]` dictionary that abstractly represents all blocks in the PoW chain with block hash as the dictionary key + * `safe_block_hash` is the return value of the `get_safe_execution_payload_hash(store: Store)` function call * `finalized_block_hash` is the hash of the latest finalized execution payload (`Hash32()` if none yet finalized) * `suggested_fee_recipient` is the value suggested to be used for the `fee_recipient` field of the execution payload @@ -120,6 +121,7 @@ To obtain an execution payload, a block proposer building a block on top of a `s ```python def prepare_execution_payload(state: BeaconState, pow_chain: Dict[Hash32, PowBlock], + safe_block_hash: Hash32, finalized_block_hash: Hash32, suggested_fee_recipient: ExecutionAddress, execution_engine: ExecutionEngine) -> Optional[PayloadId]: @@ -146,7 +148,12 @@ def prepare_execution_payload(state: BeaconState, prev_randao=get_randao_mix(state, get_current_epoch(state)), suggested_fee_recipient=suggested_fee_recipient, ) - return execution_engine.notify_forkchoice_updated(parent_hash, finalized_block_hash, payload_attributes) + return execution_engine.notify_forkchoice_updated( + head_block_hash=parent_hash, + safe_block_hash=safe_block_hash, + finalized_block_hash=finalized_block_hash, + payload_attributes=payload_attributes, + ) ``` 2. Set `block.body.execution_payload = get_execution_payload(payload_id, execution_engine)`, where: diff --git a/specs/capella/beacon-chain.md b/specs/capella/beacon-chain.md new file mode 100644 index 0000000000..f940b3a273 --- /dev/null +++ b/specs/capella/beacon-chain.md @@ -0,0 +1,428 @@ +# Capella -- The Beacon Chain + +## Table of contents + + + + + +- [Introduction](#introduction) +- [Custom types](#custom-types) +- [Constants](#constants) + - [Domain types](#domain-types) +- [Preset](#preset) + - [State list lengths](#state-list-lengths) + - [Max operations per block](#max-operations-per-block) + - [Execution](#execution) +- [Configuration](#configuration) +- [Containers](#containers) + - [New containers](#new-containers) + - [`Withdrawal`](#withdrawal) + - [`BLSToExecutionChange`](#blstoexecutionchange) + - [`SignedBLSToExecutionChange`](#signedblstoexecutionchange) + - [Extended Containers](#extended-containers) + - [`ExecutionPayload`](#executionpayload) + - [`ExecutionPayloadHeader`](#executionpayloadheader) + - [`Validator`](#validator) + - [`BeaconBlockBody`](#beaconblockbody) + - [`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) + - [Modified `process_operations`](#modified-process_operations) + - [New `process_bls_to_execution_change`](#new-process_bls_to_execution_change) + + + + +## 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 + +We define the following Python custom types for type hinting and readability: + +| Name | SSZ equivalent | Description | +| - | - | - | +| `WithdrawalIndex` | `uint64` | an index of a `Withdrawal`| + +## Constants + +### Domain types + +| Name | Value | +| - | - | +| `DOMAIN_BLS_TO_EXECUTION_CHANGE` | `DomainType('0x0A000000')` | + +## Preset + +### State list lengths + +| Name | Value | Unit | Duration | +| - | - | :-: | :-: | +| `WITHDRAWALS_QUEUE_LIMIT` | `uint64(2**40)` (= 1,099,511,627,776) | withdrawals enqueued in state| + +### Max operations per block + +| Name | Value | +| - | - | +| `MAX_BLS_TO_EXECUTION_CHANGES` | `2**4` (= 16) | + +### 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 +``` + +#### `BLSToExecutionChange` + +```python +class BLSToExecutionChange(Container): + validator_index: ValidatorIndex + from_bls_pubkey: BLSPubkey + to_execution_address: ExecutionAddress +``` + +#### `SignedBLSToExecutionChange` + +```python +class SignedBLSToExecutionChange(Container): + message: BLSToExecutionChange + signature: BLSSignature +``` + +### 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] +``` + +#### `BeaconBlockBody` + +```python +class BeaconBlockBody(Container): + randao_reveal: BLSSignature + eth1_data: Eth1Data # Eth1 data vote + graffiti: Bytes32 # Arbitrary data + # Operations + proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS] + attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS] + attestations: List[Attestation, MAX_ATTESTATIONS] + deposits: List[Deposit, MAX_DEPOSITS] + voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS] + sync_aggregate: SyncAggregate + # Execution + execution_payload: ExecutionPayload + # Capella operations + bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] # [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] + ) +``` + +#### Modified `process_operations` + +*Note*: The function `process_operations` is modified to process `BLSToExecutionChange` operations included in the block. + +```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) # [New in Capella] +``` + +#### New `process_bls_to_execution_change` + +```python +def process_bls_to_execution_change(state: BeaconState, + signed_address_change: SignedBLSToExecutionChange) -> None: + address_change = signed_address_change.message + + assert address_change.validator_index < len(state.validators) + + validator = state.validators[address_change.validator_index] + + assert validator.withdrawal_credentials[:1] == BLS_WITHDRAWAL_PREFIX + assert validator.withdrawal_credentials[1:] == hash(address_change.from_bls_pubkey)[1:] + + domain = get_domain(state, DOMAIN_BLS_TO_EXECUTION_CHANGE) + signing_root = compute_signing_root(address_change, domain) + assert bls.Verify(address_change.from_bls_pubkey, signing_root, signed_address_change.signature) + + validator.withdrawal_credentials = ( + ETH1_ADDRESS_WITHDRAWAL_PREFIX + + b'\x00' * 11 + + address_change.to_execution_address + ) +``` 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..8c6c860a38 --- /dev/null +++ b/specs/capella/validator.md @@ -0,0 +1,108 @@ +# 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], + safe_block_hash: Hash32, + 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] + ) + return execution_engine.notify_forkchoice_updated( + head_block_hash=parent_hash, + safe_block_hash=safe_block_hash, + finalized_block_hash=finalized_block_hash, + payload_attributes=payload_attributes, + ) +``` diff --git a/specs/eip4844/beacon-chain.md b/specs/eip4844/beacon-chain.md new file mode 100644 index 0000000000..8c84a28629 --- /dev/null +++ b/specs/eip4844/beacon-chain.md @@ -0,0 +1,190 @@ +# EIP-4844 -- The Beacon Chain + +**Notice**: This document is a work-in-progress for researchers and implementers. + +## Table of contents + + + + + +- [Introduction](#introduction) +- [Custom types](#custom-types) +- [Constants](#constants) + - [Domain types](#domain-types) +- [Preset](#preset) + - [Trusted setup](#trusted-setup) +- [Configuration](#configuration) +- [Containers](#containers) + - [Extended containers](#extended-containers) + - [`BeaconBlockBody`](#beaconblockbody) +- [Helper functions](#helper-functions) + - [KZG core](#kzg-core) + - [`blob_to_kzg`](#blob_to_kzg) + - [`kzg_to_versioned_hash`](#kzg_to_versioned_hash) + - [Misc](#misc) + - [`tx_peek_blob_versioned_hashes`](#tx_peek_blob_versioned_hashes) + - [`verify_kzgs_against_transactions`](#verify_kzgs_against_transactions) +- [Beacon chain state transition function](#beacon-chain-state-transition-function) + - [Block processing](#block-processing) + - [Blob KZGs](#blob-kzgs) +- [Testing](#testing) + + + + +## Introduction + +This upgrade adds blobs to the beacon chain as part of EIP-4844. + +## Custom types + +| Name | SSZ equivalent | Description | +| - | - | - | +| `BLSFieldElement` | `uint256` | `x < BLS_MODULUS` | +| `Blob` | `Vector[BLSFieldElement, FIELD_ELEMENTS_PER_BLOB]` | | +| `VersionedHash` | `Bytes32` | | +| `KZGCommitment` | `Bytes48` | Same as BLS standard "is valid pubkey" check but also allows `0x00..00` for point-at-infinity | + +## Constants + +| Name | Value | +| - | - | +| `BLOB_TX_TYPE` | `uint8(0x05)` | +| `FIELD_ELEMENTS_PER_BLOB` | `4096` | +| `BLS_MODULUS` | `52435875175126190479447740508185965837690552500527637822603658699938581184513` | + +### Domain types + +| Name | Value | +| - | - | +| `DOMAIN_BLOBS_SIDECAR` | `DomainType('0x0a000000')` | + +## Preset + +### Trusted setup + +The trusted setup is part of the preset: during testing a `minimal` insecure variant may be used, +but reusing the `mainnet` settings in public networks is a critical security requirement. + +| Name | Value | +| - | - | +| `KZG_SETUP_G2` | `Vector[G2Point, FIELD_ELEMENTS_PER_BLOB]`, contents TBD | +| `KZG_SETUP_LAGRANGE` | `Vector[KZGCommitment, FIELD_ELEMENTS_PER_BLOB]`, contents TBD | + +## Configuration + + +## Containers + +### Extended containers + +#### `BeaconBlockBody` + +Note: `BeaconBlock` and `SignedBeaconBlock` types are updated indirectly. + +```python +class BeaconBlockBody(Container): + randao_reveal: BLSSignature + eth1_data: Eth1Data # Eth1 data vote + graffiti: Bytes32 # Arbitrary data + # Operations + proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS] + attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS] + attestations: List[Attestation, MAX_ATTESTATIONS] + deposits: List[Deposit, MAX_DEPOSITS] + voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS] + sync_aggregate: SyncAggregate + # Execution + execution_payload: ExecutionPayload + blob_kzgs: List[KZGCommitment, MAX_BLOBS_PER_BLOCK] # [New in EIP-4844] +``` + +## Helper functions + +### KZG core + +KZG core functions. These are also defined in EIP-4844 execution specs. + +#### `blob_to_kzg` + +```python +def blob_to_kzg(blob: Blob) -> KZGCommitment: + computed_kzg = bls.Z1 + for value, point_kzg in zip(blob, KZG_SETUP_LAGRANGE): + assert value < BLS_MODULUS + computed_kzg = bls.add( + computed_kzg, + bls.multiply(point_kzg, value) + ) + return computed_kzg +``` + +#### `kzg_to_versioned_hash` + +```python +def kzg_to_versioned_hash(kzg: KZGCommitment) -> VersionedHash: + return BLOB_COMMITMENT_VERSION_KZG + hash(kzg)[1:] +``` + +### Misc + +#### `tx_peek_blob_versioned_hashes` + +This function retrieves the hashes from the `SignedBlobTransaction` as defined in EIP-4844, using SSZ offsets. +Offsets are little-endian `uint32` values, as defined in the [SSZ specification](../../ssz/simple-serialize.md). + +```python +def tx_peek_blob_versioned_hashes(opaque_tx: Transaction) -> Sequence[VersionedHash]: + assert opaque_tx[0] == BLOB_TX_TYPE + message_offset = 1 + uint32.decode_bytes(opaque_tx[1:5]) + # field offset: 32 + 8 + 32 + 32 + 8 + 4 + 32 + 4 + 4 = 156 + blob_versioned_hashes_offset = uint32.decode_bytes(opaque_tx[message_offset+156:message_offset+160]) + return [VersionedHash(opaque_tx[x:x+32]) for x in range(blob_versioned_hashes_offset, len(opaque_tx), 32)] +``` + +#### `verify_kzgs_against_transactions` + +```python +def verify_kzgs_against_transactions(transactions: Sequence[Transaction], blob_kzgs: Sequence[KZGCommitment]) -> bool: + all_versioned_hashes = [] + for tx in transactions: + if tx[0] == BLOB_TX_TYPE: + all_versioned_hashes.extend(tx_peek_blob_versioned_hashes(tx)) + return all_versioned_hashes == [kzg_to_versioned_hash(kzg) for kzg in blob_kzgs] +``` + +## Beacon chain state transition function + +### Block processing + +```python +def process_block(state: BeaconState, block: BeaconBlock) -> None: + process_block_header(state, block) + if is_execution_enabled(state, block.body): + process_execution_payload(state, block.body.execution_payload, EXECUTION_ENGINE) + process_randao(state, block.body) + process_eth1_data(state, block.body) + process_operations(state, block.body) + process_sync_aggregate(state, block.body.sync_aggregate) + process_blob_kzgs(state, block.body) # [New in EIP-4844] +``` + +#### Blob KZGs + +```python +def process_blob_kzgs(state: BeaconState, body: BeaconBlockBody): + assert verify_kzgs_against_transactions(body.execution_payload.transactions, body.blob_kzgs) +``` + +## Testing + +*Note*: The function `initialize_beacon_state_from_eth1` is modified for pure EIP-4844 testing only. + +The `BeaconState` initialization is unchanged, except for the use of the updated `eip4844.BeaconBlockBody` type +when initializing the first body-root: + +```python +state.latest_block_header=BeaconBlockHeader(body_root=hash_tree_root(BeaconBlockBody())), +``` + diff --git a/specs/eip4844/fork.md b/specs/eip4844/fork.md new file mode 100644 index 0000000000..ad1b00b798 --- /dev/null +++ b/specs/eip4844/fork.md @@ -0,0 +1,43 @@ +# EIP-4844 -- Fork Logic + +**Notice**: This document is a work-in-progress for researchers and implementers. + +## Table of contents + + + + +- [Introduction](#introduction) +- [Configuration](#configuration) +- [Fork to EIP-4844](#fork-to-eip-4844) + - [Fork trigger](#fork-trigger) + - [Upgrading the state](#upgrading-the-state) + + + +## Introduction + +This document describes the process of EIP-4844 upgrade. + +## Configuration + +Warning: this configuration is not definitive. + +| Name | Value | +| - | - | +| `EIP4844_FORK_VERSION` | `Version('0x03000000')` | +| `EIP4844_FORK_EPOCH` | `Epoch(18446744073709551615)` **TBD** | + +## Fork to EIP-4844 + +### 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 `EIP4844_FORK_EPOCH`. + +Note that for the pure EIP-4844 networks, we don't apply `upgrade_to_eip4844` since it starts with EIP-4844 version logic. + +### Upgrading the state + +The `eip4844.BeaconState` format is equal to the `bellatrix.BeaconState` format, no upgrade has to be performed. + diff --git a/specs/eip4844/p2p-interface.md b/specs/eip4844/p2p-interface.md new file mode 100644 index 0000000000..ff2a11e252 --- /dev/null +++ b/specs/eip4844/p2p-interface.md @@ -0,0 +1,260 @@ +# EIP-4844 -- Networking + +This document contains the consensus-layer networking specification for EIP-4844. + +The specification of these changes continues in the same format as the network specifications of previous upgrades, and assumes them as pre-requisite. + +## Table of contents + + + + + + - [Preset](#preset) + - [Configuration](#configuration) + - [Containers](#containers) + - [`BlobsSidecar`](#blobssidecar) + - [`SignedBlobsSidecar`](#signedblobssidecar) + - [The gossip domain: gossipsub](#the-gossip-domain-gossipsub) + - [Topics and messages](#topics-and-messages) + - [Global topics](#global-topics) + - [`beacon_block`](#beacon_block) + - [`blobs_sidecar`](#blobs_sidecar) + - [Transitioning the gossip](#transitioning-the-gossip) + - [The Req/Resp domain](#the-reqresp-domain) + - [Messages](#messages) + - [BeaconBlocksByRange v2](#beaconblocksbyrange-v2) + - [BeaconBlocksByRoot v2](#beaconblocksbyroot-v2) + - [BlobsSidecarsByRange v1](#blobssidecarsbyrange-v1) +- [Design decision rationale](#design-decision-rationale) + - [Why are blobs relayed as a sidecar, separate from beacon blocks?](#why-are-blobs-relayed-as-a-sidecar-separate-from-beacon-blocks) + + + + + +## Preset + +| Name | Value | +| - | - | +| `MAX_BLOBS_PER_BLOCK` | `uint64(2**4)` (= 16) | + +## Configuration + +| Name | Value | Description | +|------------------------------------------|-------------------------------|---------------------------------------------------------------------| +| `MAX_REQUEST_BLOBS_SIDECARS` | `2**7` (= 128) | Maximum number of blobs sidecars in a single request | +| `MIN_EPOCHS_FOR_BLOBS_SIDECARS_REQUESTS` | `2**13` (= 8192, ~1.2 months) | The minimum epoch range over which a node must serve blobs sidecars | + + + +## Containers + +### `BlobsSidecar` + +```python +class BlobsSidecar(Container): + beacon_block_root: Root + beacon_block_slot: Slot + blobs: List[Blob, MAX_BLOBS_PER_BLOCK] +``` + +### `SignedBlobsSidecar` + +```python +class SignedBlobsSidecar(Container): + message: BlobsSidecar + signature: BLSSignature +``` + + +## The gossip domain: gossipsub + +Some gossip meshes are upgraded in the fork of EIP4844 to support upgraded types. + +### Topics and messages + +Topics follow the same specification as in prior upgrades. +All topics remain stable except the beacon block topic which is updated with the modified type. + +The specification around the creation, validation, and dissemination of messages has not changed from the Bellatrix document unless explicitly noted here. + +The derivation of the `message-id` remains stable. + +The new topics along with the type of the `data` field of a gossipsub message are given in this table: + +| Name | Message Type | +| - | - | +| `beacon_block` | `SignedBeaconBlock` (modified) | +| `blobs_sidecar` | `SignedBlobsSidecar` (new) | + +Note that the `ForkDigestValue` path segment of the topic separates the old and the new `beacon_block` topics. + +#### Global topics + +EIP4844 changes the type of the global beacon block topic and introduces a new global topic for blobs-sidecars. + +##### `beacon_block` + +The *type* of the payload of this topic changes to the (modified) `SignedBeaconBlock` found in EIP4844. + +In addition to the gossip validations for this topic from prior specifications, +the following validations MUST pass before forwarding the `signed_beacon_block` on the network. +Alias `block = signed_beacon_block.message`, `execution_payload = block.body.execution_payload`. +- _[REJECT]_ The KZG commitments of the blobs are all correctly encoded compressed BLS G1 Points. + -- i.e. `all(bls.KeyValidate(commitment) for commitment in block.body.blob_kzgs)` +- _[REJECT]_ The KZG commitments correspond to the versioned hashes in the transactions list. + -- i.e. `verify_kzgs_against_transactions(block.body.execution_payload.transactions, block.body.blob_kzgs)` + +##### `blobs_sidecar` + +This topic is used to propagate data blobs included in any given beacon block. + +The following validations MUST pass before forwarding the `signed_blobs_sidecar` on the network; +Alias `sidecar = signed_blobs_sidecar.message`. +- _[IGNORE]_ the `sidecar.beacon_block_slot` is for the current slot (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) -- i.e. `blobs_sidecar.beacon_block_slot == current_slot`. +- _[REJECT]_ the `sidecar.blobs` are all well formatted, i.e. the `BLSFieldElement` in valid range (`x < BLS_MODULUS`). +- _[REJECT]_ the beacon proposer signature, `signed_blobs_sidecar.signature`, is valid -- i.e. +```python +domain = get_domain(state, DOMAIN_BLOBS_SIDECAR, blobs_sidecar.beacon_block_slot // SLOTS_PER_EPOCH) +signing_root = compute_signing_root(blobs_sidecar, domain) +assert bls.Verify(proposer_pubkey, signing_root, signed_blob_header.signature) +``` + where `proposer_pubkey` is the pubkey of the beacon block proposer of `blobs_sidecar.beacon_block_slot` +- _[IGNORE]_ The sidecar is the first sidecar with valid signature received for the `(proposer_index, sidecar.beacon_block_slot)` combination, + where `proposer_index` is the validator index of the beacon block proposer of `blobs_sidecar.beacon_block_slot` + +Note that a sidecar may be propagated before or after the corresponding beacon block. + +Once both sidecar and beacon block are received, `verify_blobs_sidecar` can unlock the data-availability fork-choice dependency. + +### Transitioning the gossip + +See gossip transition details found in the [Altair document](../altair/p2p-interface.md#transitioning-the-gossip) for +details on how to handle transitioning gossip topics for this upgrade. + +## The Req/Resp domain + +### Messages + +#### BeaconBlocksByRange v2 + +**Protocol ID:** `/eth2/beacon_chain/req/beacon_blocks_by_range/2/` + +The EIP-4844 fork-digest is introduced to the `context` enum to specify EIP-4844 beacon block type. + +Per `context = compute_fork_digest(fork_version, genesis_validators_root)`: + +[0]: # (eth2spec: skip) + +| `fork_version` | Chunk SSZ type | +|--------------------------|-------------------------------| +| `GENESIS_FORK_VERSION` | `phase0.SignedBeaconBlock` | +| `ALTAIR_FORK_VERSION` | `altair.SignedBeaconBlock` | +| `BELLATRIX_FORK_VERSION` | `bellatrix.SignedBeaconBlock` | +| `EIP4844_FORK_VERSION` | `eip4844.SignedBeaconBlock` | + +#### BeaconBlocksByRoot v2 + +**Protocol ID:** `/eth2/beacon_chain/req/beacon_blocks_by_root/2/` + +The EIP-4844 fork-digest is introduced to the `context` enum to specify EIP-4844 beacon block type. + +Per `context = compute_fork_digest(fork_version, genesis_validators_root)`: + +[1]: # (eth2spec: skip) + +| `fork_version` | Chunk SSZ type | +| ------------------------ | -------------------------- | +| `GENESIS_FORK_VERSION` | `phase0.SignedBeaconBlock` | +| `ALTAIR_FORK_VERSION` | `altair.SignedBeaconBlock` | +| `BELLATRIX_FORK_VERSION` | `bellatrix.SignedBeaconBlock` | +| `EIP4844_FORK_VERSION` | `eip4844.SignedBeaconBlock` | + +#### BlobsSidecarsByRange v1 + +**Protocol ID:** `/eth2/beacon_chain/req/blobs_sidecars_by_range/1/` + +Request Content: +``` +( + start_slot: Slot + count: uint64 +) +``` + +Response Content: +``` +( + List[BlobsSidecar, MAX_REQUEST_BLOBS_SIDECARS] +) +``` + +Requests blobs sidecars in the slot range `[start_slot, start_slot + count)`, +leading up to the current head block as selected by fork choice. + +The response is unsigned, i.e. `BlobsSidecarsByRange`, as the signature of the beacon block proposer +may not be available beyond the initial distribution via gossip. + +Before consuming the next response chunk, the response reader SHOULD verify the blobs sidecar is well-formatted and +correct w.r.t. the expected KZG commitments through `verify_blobs_sidecar`. + +`BlobsSidecarsByRange` is primarily used to sync blobs that may have been missed on gossip. + +The request MUST be encoded as an SSZ-container. + +The response MUST consist of zero or more `response_chunk`. +Each _successful_ `response_chunk` MUST contain a single `SignedBlobsSidecar` payload. + +Clients MUST keep a record of signed blobs sidecars seen on the epoch range +`[max(GENESIS_EPOCH, current_epoch - MIN_EPOCHS_FOR_BLOBS_SIDECARS_REQUESTS), current_epoch]` +where `current_epoch` is defined by the current wall-clock time, +and clients MUST support serving requests of blocks on this range. + +Peers that are unable to reply to block requests within the `MIN_EPOCHS_FOR_BLOBS_SIDECARS_REQUESTS` +epoch range SHOULD respond with error code `3: ResourceUnavailable`. +Such peers that are unable to successfully reply to this range of requests MAY get descored +or disconnected at any time. + +*Note*: The above requirement implies that nodes that start from a recent weak subjectivity checkpoint +MUST backfill the local blobs database to at least epoch `current_epoch - MIN_EPOCHS_FOR_BLOBS_SIDECARS_REQUESTS` +to be fully compliant with `BlobsSidecarsByRange` requests. To safely perform such a +backfill of blocks to the recent state, the node MUST validate both (1) the +proposer signatures and (2) that the blocks form a valid chain up to the most +recent block referenced in the weak subjectivity state. + +*Note*: Although clients that bootstrap from a weak subjectivity checkpoint can begin +participating in the networking immediately, other peers MAY +disconnect and/or temporarily ban such an un-synced or semi-synced client. + +Clients MUST respond with at least the first blobs sidecar that exists in the range, if they have it, +and no more than `MAX_REQUEST_BLOBS_SIDECARS` sidecars. + +The following blobs sidecars, where they exist, MUST be sent in consecutive order. + +Clients MAY limit the number of blobs sidecars in the response. + +The response MUST contain no more than `count` blobs sidecars. + +Clients MUST respond with blobs sidecars from their view of the current fork choice +-- that is, blobs sidecars as included by blocks from the single chain defined by the current head. +Of note, blocks from slots before the finalization MUST lead to the finalized block reported in the `Status` handshake. + +Clients MUST respond with blobs sidecars that are consistent from a single chain within the context of the request. + +After the initial blobs sidecar, clients MAY stop in the process of responding +if their fork choice changes the view of the chain in the context of the request. + + + +# Design decision rationale + +## Why are blobs relayed as a sidecar, separate from beacon blocks? + +This "sidecar" design provides forward compatibility for further data increases by black-boxing `is_data_available()`: +with full sharding `is_data_available()` can be replaced by data-availability-sampling (DAS) +thus avoiding all blobs being downloaded by all beacon nodes on the network. + +Such sharding design may introduce an updated `BlobsSidecar` to identify the shard, +but does not affect the `BeaconBlock` structure. + diff --git a/specs/eip4844/validator.md b/specs/eip4844/validator.md new file mode 100644 index 0000000000..2083934c54 --- /dev/null +++ b/specs/eip4844/validator.md @@ -0,0 +1,134 @@ +# EIP-4844 -- Honest Validator + +**Notice**: This document is a work-in-progress for researchers and implementers. + +## Table of contents + + + + + +- [Introduction](#introduction) +- [Prerequisites](#prerequisites) +- [Helpers](#helpers) + - [`is_data_available`](#is_data_available) + - [`verify_blobs_sidecar`](#verify_blobs_sidecar) +- [Beacon chain responsibilities](#beacon-chain-responsibilities) + - [Block proposal](#block-proposal) + - [Constructing the `BeaconBlockBody`](#constructing-the-beaconblockbody) + - [Blob commitments](#blob-commitments) + - [Beacon Block publishing time](#beacon-block-publishing-time) + + + + +## Introduction + +This document represents the changes to be made in the code of an "honest validator" to implement EIP-4844. + +## 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 EIP4844](./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 + +### `is_data_available` + +The implementation of `is_data_available` is meant to change with later sharding upgrades. +Initially, it requires every verifying actor to retrieve the matching `BlobsSidecar`, +and verify the sidecar with `verify_blobs`. + +Without the sidecar the block may be processed further optimistically, +but MUST NOT be considered valid until a valid `BlobsSidecar` has been downloaded. + +```python +def is_data_available(slot: Slot, beacon_block_root: Root, kzgs: Sequence[KZGCommitment]): + sidecar = retrieve_blobs_sidecar(slot, beacon_block_root) # implementation dependent, raises an exception if not available + verify_blobs_sidecar(slot, beacon_block_root, kzgs, sidecar) +``` + +### `verify_blobs_sidecar` + +```python +def verify_blobs_sidecar(slot: Slot, beacon_block_root: Root, + expected_kzgs: Sequence[KZGCommitment], blobs_sidecar: BlobsSidecar): + assert slot == blobs_sidecar.beacon_block_slot + assert beacon_block_root == blobs_sidecar.beacon_block_root + blobs = blobs_sidecar.blobs + assert len(expected_kzgs) == len(blobs) + for kzg, blob in zip(expected_kzgs, blobs): + assert blob_to_kzg(blob) == kzg +``` + + +## Beacon chain responsibilities + +All validator responsibilities remain unchanged other than those noted below. +Namely, the blob handling and the addition of `BlobsSidecar`. + +### Block proposal + +#### Constructing the `BeaconBlockBody` + +##### Blob commitments + +After retrieving the execution payload from the execution engine as specified in Bellatrix, +the blobs are retrieved and processed: + +```python +# execution_payload = execution_engine.get_payload(payload_id) +# block.body.execution_payload = execution_payload +# ... + +kzgs, blobs = get_blobs(payload_id) + +# Optionally sanity-check that the KZG commitments match the versioned hashes in the transactions +assert verify_kzgs_against_transactions(execution_payload.transactions, kzgs) + +# Optionally sanity-check that the KZG commitments match the blobs (as produced by the execution engine) +assert len(kzgs) == len(blobs) and [blob_to_kzg(blob) == kzg for blob, kzg in zip(blobs, kzgs)] + +# Update the block body +block.body.blob_kzgs = kzgs +``` + +The `blobs` should be held with the block in preparation of publishing. +Without the `blobs`, the published block will effectively be ignored by honest validators. + +Note: This API is *unstable*. `get_blobs` and `get_payload` may be unified. +Implementers may also retrieve blobs individually per transaction. + +### Beacon Block publishing time + +Before publishing a prepared beacon block proposal, the corresponding blobs are packaged into a sidecar object for distribution to the network: + +```python +blobs_sidecar = BlobsSidecar( + beacon_block_root=hash_tree_root(beacon_block) + beacon_block_slot=beacon_block.slot + shard=0, + blobs=blobs, +) +``` + +And then signed: + +```python +domain = get_domain(state, DOMAIN_BLOBS_SIDECAR, blobs_sidecar.beacon_block_slot / SLOTS_PER_EPOCH) +signing_root = compute_signing_root(blobs_sidecar, domain) +signature = bls.Sign(privkey, signing_root) +signed_blobs_sidecar = SignedBlobsSidecar(message=blobs_sidecar, signature=signature) +``` + +This `signed_blobs_sidecar` is then published to the global `blobs_sidecar` topic as soon as the `beacon_block` is published. + +After publishing the sidecar peers on the network may request the sidecar through sync-requests, or a local user may be interested. +The validator MUST hold on to blobs for `MIN_EPOCHS_FOR_BLOBS_SIDECARS_REQUESTS` epochs and serve when capable, +to ensure the data-availability of these blobs throughout the network. + +After `MIN_EPOCHS_FOR_BLOBS_SIDECARS_REQUESTS` nodes MAY prune the blobs and/or stop serving them. + diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index 618f396374..7e14fa951a 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -203,6 +203,9 @@ The following values are (non-configurable) constants used throughout the specif | `DOMAIN_VOLUNTARY_EXIT` | `DomainType('0x04000000')` | | `DOMAIN_SELECTION_PROOF` | `DomainType('0x05000000')` | | `DOMAIN_AGGREGATE_AND_PROOF` | `DomainType('0x06000000')` | +| `DOMAIN_APPLICATION_MASK` | `DomainType('0x00000001')` | + +*Note*: `DOMAIN_APPLICATION_MASK` reserves the rest of the bitspace in `DomainType` for application usage. This means for some `DomainType` `DOMAIN_SOME_APPLICATION`, `DOMAIN_SOME_APPLICATION & DOMAIN_APPLICATION_MASK` **MUST** be non-zero. This expression for any other `DomainType` in the consensus specs **MUST** be zero. ## Preset diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index de0a2e7856..1593e07fe0 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -32,6 +32,7 @@ - [`on_tick`](#on_tick) - [`on_block`](#on_block) - [`on_attestation`](#on_attestation) + - [`on_attester_slashing`](#on_attester_slashing) @@ -61,21 +62,21 @@ Any of the above handlers that trigger an unhandled exception (e.g. a failed ass ### Constant -| Name | Value | -| - | - | +| Name | Value | +| -------------------- | ----------- | | `INTERVALS_PER_SLOT` | `uint64(3)` | ### Preset -| Name | Value | Unit | Duration | -| - | - | :-: | :-: | +| Name | Value | Unit | Duration | +| -------------------------------- | ------------ | :---: | :--------: | | `SAFE_SLOTS_TO_UPDATE_JUSTIFIED` | `2**3` (= 8) | slots | 96 seconds | ### Configuration -| Name | Value | -| - | - | -| `PROPOSER_SCORE_BOOST` | `uint64(70)` | +| Name | Value | +| ---------------------- | ------------ | +| `PROPOSER_SCORE_BOOST` | `uint64(40)` | - The proposer score boost is worth `PROPOSER_SCORE_BOOST` percentage of the committee's weight, i.e., for slot with committee weight `committee_weight` the boost weight is equal to `(committee_weight * PROPOSER_SCORE_BOOST) // 100`. @@ -101,6 +102,7 @@ class Store(object): finalized_checkpoint: Checkpoint best_justified_checkpoint: Checkpoint proposer_boost_root: Root + equivocating_indices: Set[ValidatorIndex] blocks: Dict[Root, BeaconBlock] = field(default_factory=dict) block_states: Dict[Root, BeaconState] = field(default_factory=dict) checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict) @@ -129,6 +131,7 @@ def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) - finalized_checkpoint=finalized_checkpoint, best_justified_checkpoint=justified_checkpoint, proposer_boost_root=proposer_boost_root, + equivocating_indices=set(), blocks={anchor_root: copy(anchor_block)}, block_states={anchor_root: copy(anchor_state)}, checkpoint_states={justified_checkpoint: copy(anchor_state)}, @@ -179,6 +182,7 @@ def get_latest_attesting_balance(store: Store, root: Root) -> Gwei: attestation_score = Gwei(sum( state.validators[i].effective_balance for i in active_indices if (i in store.latest_messages + and i not in store.equivocating_indices and get_ancestor(store, store.latest_messages[i].root, store.blocks[root].slot) == root) )) if store.proposer_boost_root == Root(): @@ -357,7 +361,8 @@ def store_target_checkpoint_state(store: Store, target: Checkpoint) -> None: def update_latest_messages(store: Store, attesting_indices: Sequence[ValidatorIndex], attestation: Attestation) -> None: target = attestation.data.target beacon_block_root = attestation.data.beacon_block_root - for i in attesting_indices: + non_equivocating_attesting_indices = [i for i in attesting_indices if i not in store.equivocating_indices] + for i in non_equivocating_attesting_indices: if i not in store.latest_messages or target.epoch > store.latest_messages[i].epoch: store.latest_messages[i] = LatestMessage(epoch=target.epoch, root=beacon_block_root) ``` @@ -459,3 +464,25 @@ def on_attestation(store: Store, attestation: Attestation, is_from_block: bool=F # Update latest messages for attesting indices update_latest_messages(store, indexed_attestation.attesting_indices, attestation) ``` + +#### `on_attester_slashing` + +*Note*: `on_attester_slashing` should be called while syncing and a client MUST maintain the equivocation set of `AttesterSlashing`s from at least the latest finalized checkpoint. + +```python +def on_attester_slashing(store: Store, attester_slashing: AttesterSlashing) -> None: + """ + Run ``on_attester_slashing`` immediately upon receiving a new ``AttesterSlashing`` + from either within a block or directly on the wire. + """ + attestation_1 = attester_slashing.attestation_1 + attestation_2 = attester_slashing.attestation_2 + assert is_slashable_attestation_data(attestation_1.data, attestation_2.data) + state = store.block_states[store.justified_checkpoint.root] + assert is_valid_indexed_attestation(state, attestation_1) + assert is_valid_indexed_attestation(state, attestation_2) + + indices = set(attestation_1.attesting_indices).intersection(attestation_2.attesting_indices) + for index in indices: + store.equivocating_indices.add(index) +``` \ No newline at end of file diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index 0a0eb11925..34b0375e06 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -252,7 +252,7 @@ Likewise, clients MUST NOT emit or propagate messages larger than this limit. The optional `from` (1), `seqno` (3), `signature` (5) and `key` (6) protobuf fields are omitted from the message, since messages are identified by content, anonymous, and signed where necessary in the application layer. Starting from Gossipsub v1.1, clients MUST enforce this by applying the `StrictNoSign` -[signature policy](https://github.com/libp2p/specs/blob/master/pubsub/README.md#signature-policy-options). +[signature policy](https://github.com/libp2p/specs/blob/master/pubsub/README.md#signature-policy-options). The `message-id` of a gossipsub message MUST be the following 20 byte value computed from the message data: * If `message.data` has a valid snappy decompression, set `message-id` to the first 20 bytes of the `SHA256` hash of @@ -337,7 +337,7 @@ The following validations MUST pass before forwarding the `signed_aggregate_and_ (a client MAY queue future aggregates for processing at the appropriate slot). - _[REJECT]_ The aggregate attestation's epoch matches its target -- i.e. `aggregate.data.target.epoch == compute_epoch_at_slot(aggregate.data.slot)` -- _[IGNORE]_ The valid aggregate attestation defined by `hash_tree_root(aggregate)` has _not_ already been seen +- _[IGNORE]_ A valid aggregate attestation defined by `hash_tree_root(aggregate.data)` whose `aggregation_bits` is a non-strict superset has _not_ already been seen. (via aggregate gossip, within a verified block, or through the creation of an equivalent aggregate locally). - _[IGNORE]_ The `aggregate` is the first valid aggregate received for the aggregator with index `aggregate_and_proof.aggregator_index` for the epoch `aggregate.data.target.epoch`. @@ -355,7 +355,7 @@ The following validations MUST pass before forwarding the `signed_aggregate_and_ (via both gossip and non-gossip sources) (a client MAY queue aggregates for processing once block is retrieved). - _[REJECT]_ The block being voted for (`aggregate.data.beacon_block_root`) passes validation. -- _[REJECT]_ The current `finalized_checkpoint` is an ancestor of the `block` defined by `aggregate.data.beacon_block_root` -- i.e. +- _[IGNORE]_ The current `finalized_checkpoint` is an ancestor of the `block` defined by `aggregate.data.beacon_block_root` -- i.e. `get_ancestor(store, aggregate.data.beacon_block_root, compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)) == store.finalized_checkpoint.root` @@ -727,7 +727,7 @@ Request Content: ( start_slot: Slot count: uint64 - step: uint64 + step: uint64 # Deprecated, must be set to 1 ) ``` @@ -738,12 +738,12 @@ Response Content: ) ``` -Requests beacon blocks in the slot range `[start_slot, start_slot + count * step)`, leading up to the current head block as selected by fork choice. -`step` defines the slot increment between blocks. -For example, requesting blocks starting at `start_slot` 2 with a step value of 2 would return the blocks at slots [2, 4, 6, …]. +Requests beacon blocks in the slot range `[start_slot, start_slot + count)`, leading up to the current head block as selected by fork choice. +For example, requesting blocks starting at `start_slot=2` and `count=4` would return the blocks at slots `[2, 3, 4, 5]`. In cases where a slot is empty for a given slot number, no block is returned. -For example, if slot 4 were empty in the previous example, the returned array would contain [2, 6, …]. -A request MUST NOT have a 0 slot increment, i.e. `step >= 1`. +For example, if slot 4 were empty in the previous example, the returned array would contain `[2, 3, 5]`. + +`step` is deprecated and must be set to 1. Clients may respond with a single block if a larger step is returned during the deprecation transition period. `BeaconBlocksByRange` is primarily used to sync historical blocks. diff --git a/specs/phase0/validator.md b/specs/phase0/validator.md index e21ff980ab..1b7124e92b 100644 --- a/specs/phase0/validator.md +++ b/specs/phase0/validator.md @@ -163,10 +163,7 @@ The `withdrawal_credentials` field must be such that: * `withdrawal_credentials[12:] == eth1_withdrawal_address` After the merge of the current Ethereum application layer into the Beacon Chain, -withdrawals to `eth1_withdrawal_address` will be normal ETH transfers (with no payload other than the validator's ETH) -triggered by a user transaction that will set the gas price and gas limit as well pay fees. -As long as the account or contract with address `eth1_withdrawal_address` can receive ETH transfers, -the future withdrawal protocol is agnostic to all other implementation details. +withdrawals to `eth1_withdrawal_address` will simply be increases to the account's ETH balance that do **NOT** trigger any EVM execution. ### Submit deposit @@ -279,9 +276,15 @@ A validator has two primary responsibilities to the beacon chain: [proposing blo ### Block proposal A validator is expected to propose a [`SignedBeaconBlock`](./beacon-chain.md#signedbeaconblock) at -the beginning of any slot during which `is_proposer(state, validator_index)` returns `True`. -To propose, the validator selects the `BeaconBlock`, `parent`, -that in their view of the fork choice is the head of the chain during `slot - 1`. +the beginning of any `slot` during which `is_proposer(state, validator_index)` returns `True`. + +To propose, the validator selects the `BeaconBlock`, `parent` which: + +1. In their view of fork choice is the head of the chain at the start of + `slot`, after running `on_tick` and applying any queued attestations from `slot - 1`. +2. Is from a slot strictly less than the slot of the block about to be proposed, + i.e. `parent.slot < slot`. + The validator creates, signs, and broadcasts a `block` that is a child of `parent` that satisfies a valid [beacon chain state transition](./beacon-chain.md#beacon-chain-state-transition-function). diff --git a/sync/optimistic.md b/sync/optimistic.md index 8e6acc4427..c2f06a9be0 100644 --- a/sync/optimistic.md +++ b/sync/optimistic.md @@ -34,7 +34,6 @@ For brevity, we define two aliases for values of the `status` field on - Alias `INVALIDATED` to: - `INVALID` - `INVALID_BLOCK_HASH` - - `INVALID_TERMINAL_BLOCK` Let `head: BeaconBlock` be the result of calling of the fork choice algorithm at the time of block production. Let `head_block_root: Root` be the @@ -81,10 +80,13 @@ def is_execution_block(block: BeaconBlock) -> bool: ```python def is_optimistic_candidate_block(opt_store: OptimisticStore, current_slot: Slot, block: BeaconBlock) -> bool: - justified_root = opt_store.block_states[opt_store.head_block_root].current_justified_checkpoint.root - justified_is_execution_block = is_execution_block(opt_store.blocks[justified_root]) - block_is_deep = block.slot + SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY <= current_slot - return justified_is_execution_block or block_is_deep + if is_execution_block(opt_store.blocks[block.parent_root]): + return True + + if block.slot + SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY <= current_slot: + return True + + return False ``` Let only a node which returns `is_optimistic(opt_store, head) is True` be an *optimistic @@ -99,14 +101,20 @@ behaviours without regard for optimistic sync. ### When to optimistically import blocks A block MAY be optimistically imported when -`is_optimistic_candidate_block(opt_store, current_slot, block)` returns -`True`. This ensures that blocks are only optimistically imported if either: +`is_optimistic_candidate_block(opt_store, current_slot, block)` returns `True`. +This ensures that blocks are only optimistically imported if one or more of the +following are true: -1. The justified checkpoint has execution enabled. +1. The parent of the block has execution enabled. 1. The current slot (as per the system clock) is at least `SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY` ahead of the slot of the block being imported. +In effect, there are restrictions on when a *merge block* can be optimistically +imported. The merge block is the first block in any chain where +`is_execution_block(block) == True`. Any descendant of a merge block may be +imported optimistically at any time. + *See [Fork Choice Poisoning](#fork-choice-poisoning) for the motivations behind these conditions.* @@ -256,40 +264,9 @@ An optimistic validator MUST NOT participate in sync committees (i.e., sign acro ## Ethereum Beacon APIs Consensus engines which provide an implementation of the [Ethereum Beacon -APIs](https://github.com/ethereum/beacon-APIs) must take care to avoid -presenting optimistic blocks as fully-verified blocks. - -### Helpers - -Let the following response types be defined as any response with the -corresponding HTTP status code: - -- "Success" Response: Status Codes 200-299. -- "Not Found" Response: Status Code 404. -- "Syncing" Response: Status Code 503. - -### Requests for Optimistic Blocks - -When information about an optimistic block is requested, the consensus engine: - -- MUST NOT respond with success. -- MAY respond with not found. -- MAY respond with syncing. - -### Requests for an Optimistic Head - -When `is_optimistic(opt_store, head) is True`, the consensus engine: - -- MUST NOT return an optimistic `head`. -- MAY substitute the head block with `latest_verified_ancestor(block)`. -- MAY return syncing. - -### Requests to Validators Endpoints - -When `is_optimistic(opt_store, head) is True`, the consensus engine MUST return syncing to -all endpoints which match the following pattern: - -- `eth/*/validator/*` +APIs](https://github.com/ethereum/beacon-APIs) must take care to ensure the +`execution_optimistic` value is set to `True` whenever the request references +optimistic blocks (and vice-versa). ## Design Decision Rationale diff --git a/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py b/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py index 2ac8bfb485..32861b866f 100644 --- a/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py +++ b/tests/core/pyspec/eth2spec/test/altair/transition/test_transition.py @@ -281,7 +281,7 @@ def _drop_random_quarter(_slot, _index, indices): assert committee_len >= 4 filter_len = committee_len // 4 participant_count = committee_len - filter_len - return rng.sample(indices, participant_count) + return rng.sample(sorted(indices), participant_count) yield from _run_transition_test_with_attestations( state, @@ -304,7 +304,7 @@ def _drop_random_half(_slot, _index, indices): assert committee_len >= 2 filter_len = committee_len // 2 participant_count = committee_len - filter_len - return rng.sample(indices, participant_count) + return rng.sample(sorted(indices), participant_count) yield from _run_transition_test_with_attestations( state, diff --git a/tests/core/pyspec/eth2spec/test/altair/unittests/validator/test_validator.py b/tests/core/pyspec/eth2spec/test/altair/unittests/validator/test_validator.py index dd9214040e..1efeca5471 100644 --- a/tests/core/pyspec/eth2spec/test/altair/unittests/validator/test_validator.py +++ b/tests/core/pyspec/eth2spec/test/altair/unittests/validator/test_validator.py @@ -59,7 +59,7 @@ def test_is_assigned_to_sync_committee(spec, state): if disqualified_pubkeys: sample_size = 3 assert validator_count >= sample_size - some_pubkeys = rng.sample(disqualified_pubkeys, sample_size) + some_pubkeys = rng.sample(sorted(disqualified_pubkeys), sample_size) for pubkey in some_pubkeys: validator_index = active_pubkeys.index(pubkey) is_current = spec.is_assigned_to_sync_committee( diff --git a/tests/core/pyspec/eth2spec/test/bellatrix/unittests/validator/test_validator.py b/tests/core/pyspec/eth2spec/test/bellatrix/unittests/validator/test_validator.py index 05a941dfe4..6398aedd84 100644 --- a/tests/core/pyspec/eth2spec/test/bellatrix/unittests/validator/test_validator.py +++ b/tests/core/pyspec/eth2spec/test/bellatrix/unittests/validator/test_validator.py @@ -1,4 +1,5 @@ from copy import deepcopy +from typing import Optional from eth2spec.test.helpers.pow_block import ( prepare_random_pow_chain, @@ -142,16 +143,22 @@ def test_prepare_execution_payload(spec, state): # Dummy arguments finalized_block_hash = b'\x56' * 32 + safe_block_hash = b'\x58' * 32 suggested_fee_recipient = b'\x78' * 20 # Mock execution_engine class TestEngine(spec.NoopExecutionEngine): - def notify_forkchoice_updated(self, parent_hash, finalized_block_hash, payload_attributes) -> bool: + def notify_forkchoice_updated(self, + head_block_hash, + safe_block_hash, + finalized_block_hash, + payload_attributes) -> Optional[spec.PayloadId]: return SAMPLE_PAYLOAD_ID payload_id = spec.prepare_execution_payload( state=state, pow_chain=pow_chain.to_dict(), + safe_block_hash=safe_block_hash, finalized_block_hash=finalized_block_hash, suggested_fee_recipient=suggested_fee_recipient, execution_engine=TestEngine(), diff --git a/tests/core/pyspec/eth2spec/test/capella/block_processing/test_process_bls_to_execution_change.py b/tests/core/pyspec/eth2spec/test/capella/block_processing/test_process_bls_to_execution_change.py new file mode 100644 index 0000000000..4b69e04a63 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/capella/block_processing/test_process_bls_to_execution_change.py @@ -0,0 +1,191 @@ +from eth2spec.utils import bls +from eth2spec.test.helpers.keys import pubkeys, privkeys, pubkey_to_privkey + +from eth2spec.test.context import spec_state_test, expect_assertion_error, with_capella_and_later, always_bls + + +def run_bls_to_execution_change_processing(spec, state, signed_address_change, valid=True): + """ + Run ``process_bls_to_execution_change``, yielding: + - pre-state ('pre') + - address-change ('address_change') + - post-state ('post'). + If ``valid == False``, run expecting ``AssertionError`` + """ + # yield pre-state + yield 'pre', state + + yield 'address_change', signed_address_change + + # If the address_change is invalid, processing is aborted, and there is no post-state. + if not valid: + expect_assertion_error(lambda: spec.process_bls_to_execution_change(state, signed_address_change)) + yield 'post', None + return + + # process address change + spec.process_bls_to_execution_change(state, signed_address_change) + + # Make sure the address change has been processed + validator_index = signed_address_change.message.validator_index + validator = state.validators[validator_index] + assert validator.withdrawal_credentials[:1] == spec.ETH1_ADDRESS_WITHDRAWAL_PREFIX + assert validator.withdrawal_credentials[1:12] == b'\x00' * 11 + assert validator.withdrawal_credentials[12:] == signed_address_change.message.to_execution_address + + # yield post-state + yield 'post', state + + +def get_signed_address_change(spec, state, validator_index=None, withdrawal_pubkey=None): + if validator_index is None: + validator_index = 0 + + if withdrawal_pubkey is None: + key_index = -1 - validator_index + withdrawal_pubkey = pubkeys[key_index] + withdrawal_privkey = privkeys[key_index] + else: + withdrawal_privkey = pubkey_to_privkey[withdrawal_pubkey] + + domain = spec.get_domain(state, spec.DOMAIN_BLS_TO_EXECUTION_CHANGE) + address_change = spec.BLSToExecutionChange( + validator_index=validator_index, + from_bls_pubkey=withdrawal_pubkey, + to_execution_address=b'\x42' * 20, + ) + + signing_root = spec.compute_signing_root(address_change, domain) + return spec.SignedBLSToExecutionChange( + message=address_change, + signature=bls.Sign(withdrawal_privkey, signing_root), + ) + + +@with_capella_and_later +@spec_state_test +def test_success(spec, state): + signed_address_change = get_signed_address_change(spec, state) + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change) + + +@with_capella_and_later +@spec_state_test +def test_success_not_activated(spec, state): + validator_index = 3 + validator = state.validators[validator_index] + validator.activation_eligibility_epoch += 4 + validator.activation_epoch = spec.FAR_FUTURE_EPOCH + + assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) + + signed_address_change = get_signed_address_change(spec, state) + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change) + + assert not spec.is_fully_withdrawable_validator(state.validators[validator_index], spec.get_current_epoch(state)) + + +@with_capella_and_later +@spec_state_test +def test_success_in_activation_queue(spec, state): + validator_index = 3 + validator = state.validators[validator_index] + validator.activation_eligibility_epoch = spec.get_current_epoch(state) + validator.activation_epoch += 4 + + assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) + + signed_address_change = get_signed_address_change(spec, state) + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change) + + assert not spec.is_fully_withdrawable_validator(state.validators[validator_index], spec.get_current_epoch(state)) + + +@with_capella_and_later +@spec_state_test +def test_success_in_exit_queue(spec, state): + validator_index = 3 + spec.initiate_validator_exit(state, validator_index) + + assert spec.is_active_validator(state.validators[validator_index], spec.get_current_epoch(state)) + assert spec.get_current_epoch(state) < state.validators[validator_index].exit_epoch + + signed_address_change = get_signed_address_change(spec, state, validator_index=validator_index) + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change) + + +@with_capella_and_later +@spec_state_test +def test_success_exited(spec, state): + validator_index = 4 + validator = state.validators[validator_index] + validator.exit_epoch = spec.get_current_epoch(state) + + assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) + + signed_address_change = get_signed_address_change(spec, state, validator_index=validator_index) + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change) + + assert not spec.is_fully_withdrawable_validator(state.validators[validator_index], spec.get_current_epoch(state)) + + +@with_capella_and_later +@spec_state_test +def test_success_withdrawable(spec, state): + validator_index = 4 + validator = state.validators[validator_index] + validator.exit_epoch = spec.get_current_epoch(state) + validator.withdrawable_epoch = spec.get_current_epoch(state) + + assert not spec.is_active_validator(validator, spec.get_current_epoch(state)) + + signed_address_change = get_signed_address_change(spec, state, validator_index=validator_index) + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change) + + assert spec.is_fully_withdrawable_validator(state.validators[validator_index], spec.get_current_epoch(state)) + + +@with_capella_and_later +@spec_state_test +def test_fail_val_index_out_of_range(spec, state): + # Create for one validator beyond the validator list length + signed_address_change = get_signed_address_change(spec, state, validator_index=len(state.validators)) + + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_already_0x01(spec, state): + # Create for one validator beyond the validator list length + validator_index = len(state.validators) // 2 + validator = state.validators[validator_index] + validator.withdrawal_credentials = b'\x01' + b'\x00' * 11 + b'\x23' * 20 + signed_address_change = get_signed_address_change(spec, state, validator_index=validator_index) + + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change, valid=False) + + +@with_capella_and_later +@spec_state_test +def test_fail_incorrect_from_bls_pubkey(spec, state): + # Create for one validator beyond the validator list length + validator_index = 2 + signed_address_change = get_signed_address_change( + spec, state, + validator_index=validator_index, + withdrawal_pubkey=pubkeys[0], + ) + + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change, valid=False) + + +@with_capella_and_later +@spec_state_test +@always_bls +def test_fail_bad_signature(spec, state): + signed_address_change = get_signed_address_change(spec, state) + # Mutate sigature + signed_address_change.signature = spec.BLSSignature(b'\x42' * 96) + + yield from run_bls_to_execution_change_processing(spec, state, signed_address_change, valid=False) 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_choice.py b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py index f056b9acd0..d524060a24 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py +++ b/tests/core/pyspec/eth2spec/test/helpers/fork_choice.py @@ -92,6 +92,10 @@ def get_attestation_file_name(attestation): return f"attestation_{encode_hex(attestation.hash_tree_root())}" +def get_attester_slashing_file_name(attester_slashing): + return f"attester_slashing_{encode_hex(attester_slashing.hash_tree_root())}" + + def on_tick_and_append_step(spec, store, time, test_steps): spec.on_tick(store, time) test_steps.append({'tick': int(time)}) @@ -142,6 +146,10 @@ def add_block(spec, for attestation in signed_block.message.body.attestations: run_on_attestation(spec, store, attestation, is_from_block=True, valid=True) + # An on_block step implies receiving block's attester slashings + for attester_slashing in signed_block.message.body.attester_slashings: + run_on_attester_slashing(spec, store, attester_slashing, valid=True) + block_root = signed_block.message.hash_tree_root() assert store.blocks[block_root] == signed_block.message assert store.block_states[block_root].hash_tree_root() == signed_block.message.state_root @@ -168,6 +176,38 @@ def add_block(spec, return store.block_states[signed_block.message.hash_tree_root()] +def run_on_attester_slashing(spec, store, attester_slashing, valid=True): + if not valid: + try: + spec.on_attester_slashing(store, attester_slashing) + except AssertionError: + return + else: + assert False + + spec.on_attester_slashing(store, attester_slashing) + + +def add_attester_slashing(spec, store, attester_slashing, test_steps, valid=True): + slashing_file_name = get_attester_slashing_file_name(attester_slashing) + yield get_attester_slashing_file_name(attester_slashing), attester_slashing + + if not valid: + try: + run_on_attester_slashing(spec, store, attester_slashing) + except AssertionError: + test_steps.append({ + 'attester_slashing': slashing_file_name, + 'valid': False, + }) + return + else: + assert False + + run_on_attester_slashing(spec, store, attester_slashing) + test_steps.append({'attester_slashing': slashing_file_name}) + + def get_formatted_head_output(spec, store): head = spec.get_head(store) slot = store.blocks[head].slot 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..83994c4096 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/genesis.py +++ b/tests/core/pyspec/eth2spec/test/helpers/genesis.py @@ -1,16 +1,17 @@ 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 def build_mock_validator(spec, i: int, balance: int): - pubkey = pubkeys[i] + active_pubkey = pubkeys[i] + withdrawal_pubkey = pubkeys[-1 - i] # insecurely use pubkey as withdrawal key as well - withdrawal_credentials = spec.BLS_WITHDRAWAL_PREFIX + spec.hash(pubkey)[1:] - return spec.Validator( - pubkey=pubkeys[i], + withdrawal_credentials = spec.BLS_WITHDRAWAL_PREFIX + spec.hash(withdrawal_pubkey)[1:] + validator = spec.Validator( + pubkey=active_pubkey, withdrawal_credentials=withdrawal_credentials, activation_eligibility_epoch=spec.FAR_FUTURE_EPOCH, activation_epoch=spec.FAR_FUTURE_EPOCH, @@ -19,6 +20,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/epoch_processing/test_process_registry_updates.py b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_registry_updates.py index 6539dc92d4..9c6461fb45 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_registry_updates.py +++ b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_registry_updates.py @@ -342,3 +342,31 @@ def test_activation_queue_activation_and_ejection__exceed_scaled_churn_limit(spe churn_limit = spec.get_validator_churn_limit(state) assert churn_limit > spec.config.MIN_PER_EPOCH_CHURN_LIMIT yield from run_test_activation_queue_activation_and_ejection(spec, state, churn_limit * 2) + + +@with_all_phases +@spec_state_test +def test_invalid_large_withdrawable_epoch(spec, state): + """ + This test forces a validator into a withdrawable epoch that overflows the + epoch (uint64) type. To do this we need two validators, one validator that + already has an exit epoch and another with a low effective balance. When + calculating the withdrawable epoch for the second validator, it will + use the greatest exit epoch of all of the validators. If the first + validator is given an exit epoch between + (FAR_FUTURE_EPOCH-MIN_VALIDATOR_WITHDRAWABILITY_DELAY+1) and + (FAR_FUTURE_EPOCH-1), it will cause an overflow. + """ + assert spec.is_active_validator(state.validators[0], spec.get_current_epoch(state)) + assert spec.is_active_validator(state.validators[1], spec.get_current_epoch(state)) + + state.validators[0].exit_epoch = spec.FAR_FUTURE_EPOCH - 1 + state.validators[1].effective_balance = spec.config.EJECTION_BALANCE + + try: + yield from run_process_registry_updates(spec, state) + except ValueError: + yield 'post', None + return + + raise AssertionError('expected ValueError') diff --git a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_rewards_and_penalties.py b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_rewards_and_penalties.py index 7ff4e83d3b..2bb2974980 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_rewards_and_penalties.py +++ b/tests/core/pyspec/eth2spec/test/phase0/epoch_processing/test_process_rewards_and_penalties.py @@ -200,7 +200,10 @@ def participation_tracker(slot, comm_index, comm): @spec_state_test def test_almost_empty_attestations(spec, state): rng = Random(1234) - yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, 1)) + + def participation_fn(slot, comm_index, comm): + return rng.sample(sorted(comm), 1) + yield from run_with_participation(spec, state, participation_fn) @with_all_phases @@ -208,14 +211,20 @@ def test_almost_empty_attestations(spec, state): @leaking() def test_almost_empty_attestations_with_leak(spec, state): rng = Random(1234) - yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, 1)) + + def participation_fn(slot, comm_index, comm): + return rng.sample(sorted(comm), 1) + yield from run_with_participation(spec, state, participation_fn) @with_all_phases @spec_state_test def test_random_fill_attestations(spec, state): rng = Random(4567) - yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) // 3)) + + def participation_fn(slot, comm_index, comm): + return rng.sample(sorted(comm), len(comm) // 3) + yield from run_with_participation(spec, state, participation_fn) @with_all_phases @@ -223,14 +232,20 @@ def test_random_fill_attestations(spec, state): @leaking() def test_random_fill_attestations_with_leak(spec, state): rng = Random(4567) - yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) // 3)) + + def participation_fn(slot, comm_index, comm): + return rng.sample(sorted(comm), len(comm) // 3) + yield from run_with_participation(spec, state, participation_fn) @with_all_phases @spec_state_test def test_almost_full_attestations(spec, state): rng = Random(8901) - yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) - 1)) + + def participation_fn(slot, comm_index, comm): + return rng.sample(sorted(comm), len(comm) - 1) + yield from run_with_participation(spec, state, participation_fn) @with_all_phases @@ -238,7 +253,10 @@ def test_almost_full_attestations(spec, state): @leaking() def test_almost_full_attestations_with_leak(spec, state): rng = Random(8901) - yield from run_with_participation(spec, state, lambda slot, comm_index, comm: rng.sample(comm, len(comm) - 1)) + + def participation_fn(slot, comm_index, comm): + return rng.sample(sorted(comm), len(comm) - 1) + yield from run_with_participation(spec, state, participation_fn) @with_all_phases diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py index 5e4d247e73..e8c2dc2682 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_get_head.py @@ -8,16 +8,21 @@ with_presets, ) from eth2spec.test.helpers.attestations import get_valid_attestation, next_epoch_with_attestations -from eth2spec.test.helpers.block import build_empty_block_for_next_slot +from eth2spec.test.helpers.block import ( + apply_empty_block, + build_empty_block_for_next_slot, +) from eth2spec.test.helpers.constants import MINIMAL from eth2spec.test.helpers.fork_choice import ( - tick_and_run_on_attestation, - tick_and_add_block, + add_attester_slashing, + add_block, get_anchor_root, get_genesis_forkchoice_store_and_block, get_formatted_head_output, on_tick_and_append_step, - add_block, + add_attestation, + tick_and_run_on_attestation, + tick_and_add_block, ) from eth2spec.test.helpers.state import ( next_slots, @@ -338,3 +343,84 @@ def test_proposer_boost_correct_head(spec, state): }) yield 'steps', test_steps + + +@with_all_phases +@spec_state_test +def test_discard_equivocations(spec, state): + test_steps = [] + genesis_state = state.copy() + + # Initialization + store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state) + yield 'anchor_state', state + yield 'anchor_block', anchor_block + anchor_root = get_anchor_root(spec, state) + assert spec.get_head(store) == anchor_root + test_steps.append({ + 'checks': { + 'head': get_formatted_head_output(spec, store), + } + }) + + # Build block that serves as head before discarding equivocations + state_1 = genesis_state.copy() + next_slots(spec, state_1, 3) + block_1 = build_empty_block_for_next_slot(spec, state_1) + signed_block_1 = state_transition_and_sign_block(spec, state_1, block_1) + + # Build equivocating attestations to feed to store + state_eqv = state_1.copy() + block_eqv = apply_empty_block(spec, state_eqv, state_eqv.slot + 1) + attestation_eqv = get_valid_attestation(spec, state_eqv, slot=block_eqv.slot, signed=True) + + next_slots(spec, state_1, 1) + attestation = get_valid_attestation(spec, state_1, slot=block_eqv.slot, signed=True) + assert spec.is_slashable_attestation_data(attestation.data, attestation_eqv.data) + + indexed_attestation = spec.get_indexed_attestation(state_1, attestation) + indexed_attestation_eqv = spec.get_indexed_attestation(state_eqv, attestation_eqv) + attester_slashing = spec.AttesterSlashing(attestation_1=indexed_attestation, attestation_2=indexed_attestation_eqv) + + # Build block that serves as head after discarding equivocations + state_2 = genesis_state.copy() + next_slots(spec, state_2, 2) + block_2 = build_empty_block_for_next_slot(spec, state_2) + signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2) + while spec.hash_tree_root(block_1) >= spec.hash_tree_root(block_2): + block_2.body.graffiti = spec.Bytes32(hex(rng.getrandbits(8 * 32))[2:].zfill(64)) + signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2) + assert spec.hash_tree_root(block_1) < spec.hash_tree_root(block_2) + + # Tick to (block_eqv.slot + 2) slot time + time = store.genesis_time + (block_eqv.slot + 2) * spec.config.SECONDS_PER_SLOT + on_tick_and_append_step(spec, store, time, test_steps) + + # Process block_2 + yield from add_block(spec, store, signed_block_2, test_steps) + assert store.proposer_boost_root == spec.Root() + assert spec.get_head(store) == spec.hash_tree_root(block_2) + + # Process block_1 + # The head should remain block_2 + yield from add_block(spec, store, signed_block_1, test_steps) + assert store.proposer_boost_root == spec.Root() + assert spec.get_head(store) == spec.hash_tree_root(block_2) + + # Process attestation + # The head should change to block_1 + yield from add_attestation(spec, store, attestation, test_steps) + assert spec.get_head(store) == spec.hash_tree_root(block_1) + + # Process attester_slashing + # The head should revert to block_2 + yield from add_attester_slashing(spec, store, attester_slashing, test_steps) + assert spec.get_head(store) == spec.hash_tree_root(block_2) + + test_steps.append({ + 'checks': { + 'head': get_formatted_head_output(spec, store), + } + }) + + yield 'steps', test_steps diff --git a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_on_block.py b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_on_block.py index f57522ad76..eede246302 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_on_block.py +++ b/tests/core/pyspec/eth2spec/test/phase0/fork_choice/test_on_block.py @@ -38,7 +38,7 @@ def _drop_random_one_third(_slot, _index, indices): assert committee_len >= 3 filter_len = committee_len // 3 participant_count = committee_len - filter_len - return rng.sample(indices, participant_count) + return rng.sample(sorted(indices), participant_count) @with_all_phases 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, diff --git a/tests/formats/epoch_processing/README.md b/tests/formats/epoch_processing/README.md index 1032026a63..e0e6dc3675 100644 --- a/tests/formats/epoch_processing/README.md +++ b/tests/formats/epoch_processing/README.md @@ -21,7 +21,7 @@ An SSZ-snappy encoded `BeaconState`, the state before running the epoch sub-tran ### `post.ssz_snappy` -An SSZ-snappy encoded `BeaconState`, the state after applying the epoch sub-transition. +An SSZ-snappy encoded `BeaconState`, the state after applying the epoch sub-transition. No value if the sub-transition processing is aborted. ## Condition diff --git a/tests/formats/fork_choice/README.md b/tests/formats/fork_choice/README.md index e4da31a9b3..3266ad4c00 100644 --- a/tests/formats/fork_choice/README.md +++ b/tests/formats/fork_choice/README.md @@ -76,11 +76,27 @@ Adds `PowBlock` data which is required for executing `on_block(store, block)`. { pow_block: string -- the name of the `pow_block_<32-byte-root>.ssz_snappy` file. To be used in `get_pow_block` lookup -} +} ``` The file is located in the same folder (see below). PowBlocks should be used as return values for `get_pow_block(hash: Hash32) -> PowBlock` function if hashes match. +#### `on_attester_slashing` execution step + +The parameter that is required for executing `on_attester_slashing(store, attester_slashing)`. + +```yaml +{ + attester_slashing: string -- the name of the `attester_slashing_<32-byte-root>.ssz_snappy` file. + To execute `on_attester_slashing(store, attester_slashing)` with the given attester slashing. + valid: bool -- optional, default to `true`. + If it's `false`, this execution step is expected to be invalid. +} +``` +The file is located in the same folder (see below). + +After this step, the `store` object may have been updated. + #### Checks step The checks to verify the current status of `store`.