diff --git a/.circleci/config.yml b/.circleci/config.yml index fcdf483d50..157c56ca5f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -176,7 +176,7 @@ jobs: - checkout - run: name: Check table of contents - command: sudo npm install -g doctoc@2 && make check_toc + command: sudo npm install -g doctoc@2.2.0 && make check_toc codespell: docker: - image: circleci/python:3.9 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 41a80ab925..6b24ef5eb1 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -6,10 +6,8 @@ defaults: env: TEST_PRESET_TYPE: "minimal" - DEFAULT_BRANCH: "dev" -# Run tests on workflow_Dispatch -on: +on: push: branches: - dev @@ -22,10 +20,6 @@ on: description: Type of test to run, either mainnet or minimal type: string required: true - commitRef: - description: The branch, tag or SHA to checkout and build from - default: dev - required: true schedule: - cron: '0 0 * * *' @@ -47,10 +41,8 @@ jobs: steps: - name: Checkout this repo uses: actions/checkout@v3.2.0 - with: - ref: ${{ github.event.inputs.commitRef || env.DEFAULT_BRANCH }} - name: Check table of contents - run: sudo npm install -g doctoc@2 && make check_toc + run: sudo npm install -g doctoc@2.2.0 && make check_toc codespell: runs-on: self-hosted @@ -58,8 +50,6 @@ jobs: steps: - name: Checkout this repo uses: actions/checkout@v3.2.0 - with: - ref: ${{ github.event.inputs.commitRef || env.DEFAULT_BRANCH }} - name: Check codespell run: pip install 'codespell<3.0.0,>=2.0.0' --user && make codespell @@ -69,8 +59,6 @@ jobs: steps: - name: Checkout this repo uses: actions/checkout@v3.2.0 - with: - ref: ${{ github.event.inputs.commitRef || env.DEFAULT_BRANCH }} - name: Install pyspec requirements run: make install_test - name: Run linter for pyspec @@ -87,8 +75,6 @@ jobs: steps: - name: Checkout this repo uses: actions/checkout@v3.2.0 - with: - ref: ${{ github.event.inputs.commitRef || env.DEFAULT_BRANCH }} - name: set TEST_PRESET_TYPE if: github.event.inputs.test_preset_type != '' run: | diff --git a/.gitignore b/.gitignore index c7012d51b3..2ff10cf099 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ venv .venvs .venv /.pytest_cache +*.swp build/ output/ diff --git a/configs/mainnet.yaml b/configs/mainnet.yaml index 365bc11367..9206ab77da 100644 --- a/configs/mainnet.yaml +++ b/configs/mainnet.yaml @@ -53,6 +53,9 @@ DENEB_FORK_EPOCH: 18446744073709551615 # EIP6110 EIP6110_FORK_VERSION: 0x05000000 # temporary stub EIP6110_FORK_EPOCH: 18446744073709551615 +# WHISK +WHISK_FORK_VERSION: 0x06000000 # temporary stub +WHISK_FORK_EPOCH: 18446744073709551615 # Time parameters diff --git a/configs/minimal.yaml b/configs/minimal.yaml index b22a7165e1..256a39d1c1 100644 --- a/configs/minimal.yaml +++ b/configs/minimal.yaml @@ -52,6 +52,9 @@ DENEB_FORK_EPOCH: 18446744073709551615 # EIP6110 EIP6110_FORK_VERSION: 0x05000001 EIP6110_FORK_EPOCH: 18446744073709551615 +# WHISK +WHISK_FORK_VERSION: 0x06000001 +WHISK_FORK_EPOCH: 18446744073709551615 # Time parameters diff --git a/presets/mainnet/whisk.yaml b/presets/mainnet/whisk.yaml new file mode 100644 index 0000000000..3086ff29de --- /dev/null +++ b/presets/mainnet/whisk.yaml @@ -0,0 +1,20 @@ +# Mainnet preset - Whisk + +# Misc +# --------------------------------------------------------------- +# `uint64(4)` +CURDLEPROOFS_N_BLINDERS: 4 +# `uint64(2**14)` +WHISK_CANDIDATE_TRACKERS_COUNT: 16384 +# `uint64(2**13)` must be < WHISK_CANDIDATE_TRACKERS_COUNT +WHISK_PROPOSER_TRACKERS_COUNT: 8192 +# `Epoch(2**8)` +WHISK_EPOCHS_PER_SHUFFLING_PHASE: 256 +# `uint64(2**7 - CURDLEPROOFS_N_BLINDERS)` +WHISK_VALIDATORS_PER_SHUFFLE: 124 +# `Epoch(2)` +WHISK_PROPOSER_SELECTION_GAP: 2 +# `uint64(2**15)` TODO: will be replaced by a fix format once there's a serialized format +WHISK_MAX_SHUFFLE_PROOF_SIZE: 32768 +# `uint64(2**10)` TODO: will be replaced by a fix format once there's a serialized format +WHISK_MAX_OPENING_PROOF_SIZE: 1024 diff --git a/presets/minimal/whisk.yaml b/presets/minimal/whisk.yaml new file mode 100644 index 0000000000..1a726f79c2 --- /dev/null +++ b/presets/minimal/whisk.yaml @@ -0,0 +1,20 @@ +# Minimal preset - Whisk + +# Misc +# --------------------------------------------------------------- +# [customized] +CURDLEPROOFS_N_BLINDERS: 4 +# [customized] +WHISK_CANDIDATE_TRACKERS_COUNT: 32 +# [customized] +WHISK_PROPOSER_TRACKERS_COUNT: 16 +# [customized] +WHISK_EPOCHS_PER_SHUFFLING_PHASE: 4 +# [customized] +WHISK_VALIDATORS_PER_SHUFFLE: 4 +# [customized] +WHISK_PROPOSER_SELECTION_GAP: 1 +# `uint64(2**15)` TODO: will be replaced by a fix format once there's a serialized format +WHISK_MAX_SHUFFLE_PROOF_SIZE: 32768 +# `uint64(2**10)` TODO: will be replaced by a fix format once there's a serialized format +WHISK_MAX_OPENING_PROOF_SIZE: 1024 diff --git a/setup.py b/setup.py index f12800a621..5dfe29c2ae 100644 --- a/setup.py +++ b/setup.py @@ -726,7 +726,9 @@ def execution_engine_cls(cls) -> str: return "\n\n" + """ class NoopExecutionEngine(ExecutionEngine): - def notify_new_payload(self: ExecutionEngine, execution_payload: ExecutionPayload) -> bool: + def notify_new_payload(self: ExecutionEngine, + execution_payload: ExecutionPayload, + parent_beacon_block_root: Root) -> bool: return True def notify_forkchoice_updated(self: ExecutionEngine, @@ -740,7 +742,9 @@ def get_payload(self: ExecutionEngine, payload_id: PayloadId) -> GetPayloadRespo # pylint: disable=unused-argument raise NotImplementedError("no default block production") - def is_valid_block_hash(self: ExecutionEngine, execution_payload: ExecutionPayload) -> bool: + def is_valid_block_hash(self: ExecutionEngine, + execution_payload: ExecutionPayload, + parent_beacon_block_root: Root) -> bool: return True def is_valid_versioned_hashes(self: ExecutionEngine, new_payload_request: NewPayloadRequest) -> bool: @@ -776,7 +780,6 @@ def imports(cls, preset_name: str): from eth2spec.deneb import {preset_name} as deneb ''' - # # WhiskSpecBuilder # @@ -793,15 +796,18 @@ def imports(cls, preset_name: str): def hardcoded_custom_type_dep_constants(cls, spec_object) -> str: # Necessary for custom types `WhiskShuffleProof` and `WhiskTrackerProof` constants = { - 'WHISK_MAX_SHUFFLE_PROOF_SIZE': spec_object.constant_vars['WHISK_MAX_SHUFFLE_PROOF_SIZE'].value, - 'WHISK_MAX_OPENING_PROOF_SIZE': spec_object.constant_vars['WHISK_MAX_OPENING_PROOF_SIZE'].value, + 'WHISK_MAX_SHUFFLE_PROOF_SIZE': spec_object.preset_vars['WHISK_MAX_SHUFFLE_PROOF_SIZE'].value, + 'WHISK_MAX_OPENING_PROOF_SIZE': spec_object.preset_vars['WHISK_MAX_OPENING_PROOF_SIZE'].value, } return {**super().hardcoded_custom_type_dep_constants(spec_object), **constants} spec_builders = { builder.fork: builder - for builder in (Phase0SpecBuilder, AltairSpecBuilder, BellatrixSpecBuilder, CapellaSpecBuilder, DenebSpecBuilder, EIP6110SpecBuilder, WhiskSpecBuilder) + for builder in ( + Phase0SpecBuilder, AltairSpecBuilder, BellatrixSpecBuilder, CapellaSpecBuilder, DenebSpecBuilder, + EIP6110SpecBuilder, WhiskSpecBuilder, + ) } diff --git a/specs/_features/eip4788/beacon-chain.md b/specs/_features/eip4788/beacon-chain.md deleted file mode 100644 index 6cd876de99..0000000000 --- a/specs/_features/eip4788/beacon-chain.md +++ /dev/null @@ -1,72 +0,0 @@ -# EIP-4788 -- The Beacon Chain - -## Table of contents - - - - - -- [Introduction](#introduction) -- [Containers](#containers) - - [Extended Containers](#extended-containers) - - [`ExecutionPayload`](#executionpayload) - - [`ExecutionPayloadHeader`](#executionpayloadheader) - - - - -## Introduction - -TODO - -## Containers - -### 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] - parent_beacon_block_root: Root # [New in EIP-4788] -``` - -#### `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 - parent_beacon_block_root: Root # [New in EIP-4788] -``` diff --git a/specs/_features/eip4788/validator.md b/specs/_features/eip4788/validator.md deleted file mode 100644 index 11462bda1d..0000000000 --- a/specs/_features/eip4788/validator.md +++ /dev/null @@ -1,88 +0,0 @@ -# EIP-4788 -- 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) - - [Modified `get_payload`](#modified-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 EIP-4788 feature. - -## Prerequisites - -This document is an extension of the [Capella -- Honest Validator](../capella/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](../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` - -#### Modified `get_payload` - -`get_payload` returns the upgraded EIP-4788 `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 Capella, except that the parent beacon block root is also supplied. - -*Note*: In this section, `state` is the state of the slot for the block proposal _without_ the block yet applied. -That is, `state` is the `previous_state` processed through any empty slots up to the assigned slot using `process_slots(previous_state, slot)`. - -*Note*: The only change made to `prepare_execution_payload` is to add the parent beacon block root as an additional -parameter to the `PayloadAttributes`. - -```python -def prepare_execution_payload(state: BeaconState, - safe_block_hash: Hash32, - finalized_block_hash: Hash32, - suggested_fee_recipient: ExecutionAddress, - execution_engine: ExecutionEngine) -> Optional[PayloadId]: - # Verify consistency of the parent hash with respect to the previous execution payload header - 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), - parent_beacon_block_root=hash_tree_root(state.latest_block_header), # [New in EIP-4788] - ) - 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/_features/eip6110/beacon-chain.md b/specs/_features/eip6110/beacon-chain.md index 1bfa29138a..44980685c4 100644 --- a/specs/_features/eip6110/beacon-chain.md +++ b/specs/_features/eip6110/beacon-chain.md @@ -224,7 +224,7 @@ def process_deposit_receipt(state: BeaconState, deposit_receipt: DepositReceipt) state.deposit_receipts_start_index = deposit_receipt.index apply_deposit( - state=state, + state=state, pubkey=deposit_receipt.pubkey, withdrawal_credentials=deposit_receipt.withdrawal_credentials, amount=deposit_receipt.amount, @@ -251,7 +251,11 @@ def process_execution_payload(state: BeaconState, body: BeaconBlockBody, executi # Verify the execution payload is valid versioned_hashes = [kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments] assert execution_engine.verify_and_notify_new_payload( - NewPayloadRequest(execution_payload=payload, versioned_hashes=versioned_hashes) + NewPayloadRequest( + execution_payload=payload, + versioned_hashes=versioned_hashes, + parent_beacon_block_root=state.latest_block_header.parent_root, + ) ) # Cache execution payload header state.latest_execution_payload_header = ExecutionPayloadHeader( diff --git a/specs/_features/whisk/beacon-chain.md b/specs/_features/whisk/beacon-chain.md index bad8188378..f2a51e6223 100644 --- a/specs/_features/whisk/beacon-chain.md +++ b/specs/_features/whisk/beacon-chain.md @@ -10,12 +10,13 @@ - [Introduction](#introduction) - [Constants](#constants) + - [Domain types](#domain-types) +- [Preset](#preset) - [Cryptography](#cryptography) - [BLS](#bls) - [Curdleproofs and opening proofs](#curdleproofs-and-opening-proofs) - [Epoch processing](#epoch-processing) - [`WhiskTracker`](#whisktracker) - - [`Validator`](#validator) - [`BeaconState`](#beaconstate) - [Block processing](#block-processing) - [Block header](#block-header) @@ -23,6 +24,7 @@ - [`BeaconBlockBody`](#beaconblockbody) - [Deposits](#deposits) - [`get_beacon_proposer_index`](#get_beacon_proposer_index) +- [Testing](#testing) @@ -35,22 +37,27 @@ This document details the beacon chain additions and changes of to support the W ## Constants +### Domain types + +| Name | Value | +| ---------------------------------- | -------------------------- | +| `DOMAIN_WHISK_CANDIDATE_SELECTION` | `DomainType('0x07000000')` | +| `DOMAIN_WHISK_SHUFFLE` | `DomainType('0x07100000')` | +| `DOMAIN_WHISK_PROPOSER_SELECTION` | `DomainType('0x07200000')` | + +## Preset + | Name | Value | Description | | ---------------------------------- | -------------------------- | ----------------------------------------------------------- | +| `CURDLEPROOFS_N_BLINDERS` | `uint64(4)` | number of blinders for curdleproofs | | `WHISK_CANDIDATE_TRACKERS_COUNT` | `uint64(2**14)` (= 16,384) | number of candidate trackers | | `WHISK_PROPOSER_TRACKERS_COUNT` | `uint64(2**13)` (= 8,192) | number of proposer trackers | | `WHISK_EPOCHS_PER_SHUFFLING_PHASE` | `Epoch(2**8)` (= 256) | epochs per shuffling phase | -| `WHISK_VALIDATORS_PER_SHUFFLE` | `uint64(2**7)` (= 128) | number of validators shuffled per shuffle step | +| `WHISK_VALIDATORS_PER_SHUFFLE` | `uint64(2**7 - 4)` (= 124) | number of validators shuffled per shuffle step | | `WHISK_PROPOSER_SELECTION_GAP` | `Epoch(2)` | gap between proposer selection and the block proposal phase | | `WHISK_MAX_SHUFFLE_PROOF_SIZE` | `uint64(2**15)` | max size of a shuffle proof | | `WHISK_MAX_OPENING_PROOF_SIZE` | `uint64(2**10)` | max size of a opening proof | -| Name | Value | -| ---------------------------------- | -------------------------- | -| `DOMAIN_WHISK_CANDIDATE_SELECTION` | `DomainType('0x07000000')` | -| `DOMAIN_WHISK_SHUFFLE` | `DomainType('0x07100000')` | -| `DOMAIN_WHISK_PROPOSER_SELECTION` | `DomainType('0x07200000')` | - ## Cryptography ### BLS @@ -124,23 +131,6 @@ class WhiskTracker(Container): k_r_G: BLSG1Point # k * r * G ``` -### `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 - whisk_tracker: WhiskTracker # Whisk tracker (r * G, k * r * G) [New in Whisk] - whisk_k_commitment: BLSG1Point # Whisk k commitment k * BLS_G1_GENERATOR [New in Whisk] -``` - ### `BeaconState` ```python @@ -186,31 +176,42 @@ class BeaconState(Container): next_withdrawal_validator_index: ValidatorIndex # Deep history valid from Capella onwards historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] + # Whisk whisk_candidate_trackers: Vector[WhiskTracker, WHISK_CANDIDATE_TRACKERS_COUNT] # [New in Whisk] whisk_proposer_trackers: Vector[WhiskTracker, WHISK_PROPOSER_TRACKERS_COUNT] # [New in Whisk] + whisk_trackers: List[WhiskTracker, VALIDATOR_REGISTRY_LIMIT] # [New in Whisk] + whisk_k_commitments: List[BLSG1Point, VALIDATOR_REGISTRY_LIMIT] # [New in Whisk] ``` ```python -def select_whisk_trackers(state: BeaconState, epoch: Epoch) -> None: +def select_whisk_proposer_trackers(state: BeaconState, epoch: Epoch) -> None: # Select proposer trackers from candidate trackers - proposer_seed = get_seed(state, epoch - WHISK_PROPOSER_SELECTION_GAP, DOMAIN_WHISK_PROPOSER_SELECTION) + proposer_seed = get_seed( + state, + Epoch(saturating_sub(epoch, WHISK_PROPOSER_SELECTION_GAP)), + DOMAIN_WHISK_PROPOSER_SELECTION + ) for i in range(WHISK_PROPOSER_TRACKERS_COUNT): index = compute_shuffled_index(uint64(i), uint64(len(state.whisk_candidate_trackers)), proposer_seed) state.whisk_proposer_trackers[i] = state.whisk_candidate_trackers[index] +``` +```python +def select_whisk_candidate_trackers(state: BeaconState, epoch: Epoch) -> None: # Select candidate trackers from active validator trackers active_validator_indices = get_active_validator_indices(state, epoch) for i in range(WHISK_CANDIDATE_TRACKERS_COUNT): - seed = hash(get_seed(state, epoch, DOMAIN_WHISK_CANDIDATE_SELECTION) + uint_to_bytes(i)) + seed = hash(get_seed(state, epoch, DOMAIN_WHISK_CANDIDATE_SELECTION) + uint_to_bytes(uint64(i))) candidate_index = compute_proposer_index(state, active_validator_indices, seed) # sample by effective balance - state.whisk_candidate_trackers[i] = state.validators[candidate_index].whisk_tracker + state.whisk_candidate_trackers[i] = state.whisk_trackers[candidate_index] ``` ```python def process_whisk_updates(state: BeaconState) -> None: next_epoch = Epoch(get_current_epoch(state) + 1) if next_epoch % WHISK_EPOCHS_PER_SHUFFLING_PHASE == 0: # select trackers at the start of shuffling phases - select_whisk_trackers(state, next_epoch) + select_whisk_proposer_trackers(state, next_epoch) + select_whisk_candidate_trackers(state, next_epoch) ``` ```python @@ -237,7 +238,7 @@ def process_epoch(state: BeaconState) -> None: ```python def process_whisk_opening_proof(state: BeaconState, block: BeaconBlock) -> None: tracker = state.whisk_proposer_trackers[state.slot % WHISK_PROPOSER_TRACKERS_COUNT] - k_commitment = state.validators[block.proposer_index].whisk_k_commitment + k_commitment = state.whisk_k_commitments[block.proposer_index] assert IsValidWhiskOpeningProof(tracker, k_commitment, block.body.whisk_opening_proof) ``` @@ -297,7 +298,7 @@ class BeaconBlockBody(capella.BeaconBlockBody): whisk_shuffle_proof_M_commitment: BLSG1Point # [New in Whisk] whisk_registration_proof: WhiskTrackerProof # [New in Whisk] whisk_tracker: WhiskTracker # [New in Whisk] - whisk_k_commitment: BLSG1Point # [New in Whisk] + whisk_k_commitment: BLSG1Point # k * BLS_G1_GENERATOR [New in Whisk] ``` ```python @@ -306,9 +307,10 @@ def get_shuffle_indices(randao_reveal: BLSSignature) -> Sequence[uint64]: Given a `randao_reveal` return the list of indices that got shuffled from the entire candidate set """ indices = [] - for i in WHISK_VALIDATORS_PER_SHUFFLE: + for i in range(0, WHISK_VALIDATORS_PER_SHUFFLE): # XXX ensure we are not suffering from modulo bias - shuffle_index = uint256(hash(randao_reveal + uint_to_bytes(i))) % WHISK_CANDIDATE_TRACKERS_COUNT + pre_image = randao_reveal + uint_to_bytes(uint64(i)) + shuffle_index = bytes_to_uint64(hash(pre_image)[0:8]) % WHISK_CANDIDATE_TRACKERS_COUNT indices.append(shuffle_index) return indices @@ -319,20 +321,23 @@ def process_shuffled_trackers(state: BeaconState, body: BeaconBlockBody) -> None # Check the shuffle proof shuffle_indices = get_shuffle_indices(body.randao_reveal) pre_shuffle_trackers = [state.whisk_candidate_trackers[i] for i in shuffle_indices] - post_shuffle_trackers = body.whisk_post_shuffle_trackers shuffle_epoch = get_current_epoch(state) % WHISK_EPOCHS_PER_SHUFFLING_PHASE if shuffle_epoch + WHISK_PROPOSER_SELECTION_GAP + 1 >= WHISK_EPOCHS_PER_SHUFFLING_PHASE: - # Require unchanged trackers during cooldown - assert pre_shuffle_trackers == post_shuffle_trackers + # Require trackers set to zero during cooldown + assert body.whisk_post_shuffle_trackers == Vector[WhiskTracker, WHISK_VALIDATORS_PER_SHUFFLE]() + assert body.whisk_shuffle_proof_M_commitment == BLSG1Point() + assert body.whisk_shuffle_proof == WhiskShuffleProof() + post_shuffle_trackers = pre_shuffle_trackers else: # Require shuffled trackers during shuffle assert IsValidWhiskShuffleProof( pre_shuffle_trackers, - post_shuffle_trackers, + body.whisk_post_shuffle_trackers, body.whisk_shuffle_proof_M_commitment, body.whisk_shuffle_proof, ) + post_shuffle_trackers = body.whisk_post_shuffle_trackers # Shuffle candidate trackers for i, shuffle_index in enumerate(shuffle_indices): @@ -341,7 +346,7 @@ def process_shuffled_trackers(state: BeaconState, body: BeaconBlockBody) -> None ```python def is_k_commitment_unique(state: BeaconState, k_commitment: BLSG1Point) -> bool: - return all([validator.whisk_k_commitment != k_commitment for validator in state.validators]) + return all([whisk_k_commitment != k_commitment for whisk_k_commitment in state.whisk_k_commitments]) ``` ```python @@ -382,49 +387,30 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None: ### Deposits +```python +def get_initial_whisk_k(validator_index: ValidatorIndex, counter: int) -> BLSFieldElement: + # hash `validator_index || counter` + return BLSFieldElement(bytes_to_bls_field(hash(uint_to_bytes(validator_index) + uint_to_bytes(uint64(counter))))) +``` + ```python def get_unique_whisk_k(state: BeaconState, validator_index: ValidatorIndex) -> BLSFieldElement: counter = 0 while True: - # hash `validator_index || counter` - k = BLSFieldElement(bytes_to_bls_field(hash(uint_to_bytes(validator_index) + uint_to_bytes(uint64(counter))))) + k = get_initial_whisk_k(validator_index, counter) if is_k_commitment_unique(state, BLSG1ScalarMultiply(k, BLS_G1_GENERATOR)): return k # unique by trial and error counter += 1 ``` ```python -def get_initial_commitments(k: BLSFieldElement) -> Tuple[BLSG1Point, WhiskTracker]: - return ( - BLSG1ScalarMultiply(k, BLS_G1_GENERATOR), - WhiskTracker(r_G=BLS_G1_GENERATOR, k_r_G=BLSG1ScalarMultiply(k, BLS_G1_GENERATOR)) - ) +def get_k_commitment(k: BLSFieldElement) -> BLSG1Point: + return BLSG1ScalarMultiply(k, BLS_G1_GENERATOR) ``` ```python -def get_validator_from_deposit_whisk( - state: BeaconState, - pubkey: BLSPubkey, - withdrawal_credentials: Bytes32, - amount: uint64 -) -> Validator: - effective_balance = min(amount - amount % EFFECTIVE_BALANCE_INCREMENT, MAX_EFFECTIVE_BALANCE) - k = get_unique_whisk_k(state, ValidatorIndex(len(state.validators))) - whisk_k_commitment, whisk_tracker = get_initial_commitments(k) - - validator = Validator( - pubkey=pubkey, - withdrawal_credentials=withdrawal_credentials, - activation_eligibility_epoch=FAR_FUTURE_EPOCH, - activation_epoch=FAR_FUTURE_EPOCH, - exit_epoch=FAR_FUTURE_EPOCH, - withdrawable_epoch=FAR_FUTURE_EPOCH, - effective_balance=effective_balance, - # Whisk fields - whisk_tracker=whisk_tracker, - whisk_k_commitment=whisk_k_commitment, - ) - return validator +def get_initial_tracker(k: BLSFieldElement) -> WhiskTracker: + return WhiskTracker(r_G=BLS_G1_GENERATOR, k_r_G=BLSG1ScalarMultiply(k, BLS_G1_GENERATOR)) ``` ```python @@ -446,13 +432,16 @@ def apply_deposit(state: BeaconState, # Initialize validator if the deposit signature is valid if bls.Verify(pubkey, signing_root, signature): index = get_index_for_new_validator(state) - validator = get_validator_from_deposit_whisk(state, pubkey, withdrawal_credentials, amount) + validator = get_validator_from_deposit(pubkey, withdrawal_credentials, amount) set_or_append_list(state.validators, index, validator) set_or_append_list(state.balances, index, amount) - # [New in Altair] set_or_append_list(state.previous_epoch_participation, index, ParticipationFlags(0b0000_0000)) set_or_append_list(state.current_epoch_participation, index, ParticipationFlags(0b0000_0000)) set_or_append_list(state.inactivity_scores, index, uint64(0)) + # [New in Whisk] + k = get_unique_whisk_k(state, ValidatorIndex(len(state.validators) - 1)) + state.whisk_trackers.append(get_initial_tracker(k)) + state.whisk_k_commitments.append(get_k_commitment(k)) else: # Increase balance by deposit amount index = ValidatorIndex(validator_pubkeys.index(pubkey)) @@ -469,3 +458,25 @@ def get_beacon_proposer_index(state: BeaconState) -> ValidatorIndex: assert state.latest_block_header.slot == state.slot # sanity check `process_block_header` has been called return state.latest_block_header.proposer_index ``` + +## Testing + +*Note*: The function `initialize_beacon_state_from_eth1` is modified for pure Whisk testing only. + +```python +def initialize_beacon_state_from_eth1(eth1_block_hash: Hash32, + eth1_timestamp: uint64, + deposits: Sequence[Deposit], + execution_payload_header: ExecutionPayloadHeader=ExecutionPayloadHeader() + ) -> BeaconState: + state_capella = capella.initialize_beacon_state_from_eth1( + eth1_block_hash, + eth1_timestamp, + deposits, + execution_payload_header, + ) + state = upgrade_to_whisk(state_capella) + state.fork.previous_version = WHISK_FORK_VERSION + state.fork.current_version = WHISK_FORK_VERSION + return state +``` diff --git a/specs/_features/whisk/fork.md b/specs/_features/whisk/fork.md index 189d8c5b32..ef3eb08469 100644 --- a/specs/_features/whisk/fork.md +++ b/specs/_features/whisk/fork.md @@ -53,22 +53,13 @@ The upgrade occurs after the completion of the inner loop of `process_slots` tha This ensures that we drop right into the beginning of the shuffling phase but without `process_whisk_epoch()` triggering for this Whisk run. Hence we handle all the setup ourselves in `upgrade_to_whisk()` below. ```python -def whisk_candidate_selection(state: BeaconState, epoch: Epoch) -> None: - # TODO - # pylint: disable=unused-argument - pass -``` - -```python -def whisk_proposer_selection(state: BeaconState, epoch: Epoch) -> None: - # TODO - # pylint: disable=unused-argument - pass -``` +def upgrade_to_whisk(pre: capella.BeaconState) -> BeaconState: + # Compute initial unsafe trackers for all validators + ks = [get_initial_whisk_k(ValidatorIndex(validator_index), 0) for validator_index in range(len(pre.validators))] + whisk_k_commitments = [get_k_commitment(k) for k in ks] + whisk_trackers = [get_initial_tracker(k) for k in ks] -```python -def upgrade_to_whisk(pre: bellatrix.BeaconState) -> BeaconState: - epoch = bellatrix.get_current_epoch(pre) + epoch = get_current_epoch(pre) post = BeaconState( # Versioning genesis_time=pre.genesis_time, @@ -104,35 +95,32 @@ def upgrade_to_whisk(pre: bellatrix.BeaconState) -> BeaconState: current_justified_checkpoint=pre.current_justified_checkpoint, finalized_checkpoint=pre.finalized_checkpoint, # Inactivity - inactivity_scores=pre.inactivity_Scores, + inactivity_scores=pre.inactivity_scores, + # Sync + current_sync_committee=pre.current_sync_committee, + next_sync_committee=pre.next_sync_committee, + # Execution-layer + latest_execution_payload_header=pre.latest_execution_payload_header, + # Withdrawals + next_withdrawal_index=pre.next_withdrawal_index, + next_withdrawal_validator_index=pre.next_withdrawal_validator_index, + # Deep history valid from Capella onwards + historical_summaries=pre.historical_summaries, + # Whisk + whisk_proposer_trackers=[WhiskTracker() for _ in range(WHISK_PROPOSER_TRACKERS_COUNT)], # [New in Whisk] + whisk_candidate_trackers=[WhiskTracker() for _ in range(WHISK_CANDIDATE_TRACKERS_COUNT)], # [New in Whisk] + whisk_trackers=whisk_trackers, # [New in Whisk] + whisk_k_commitments=whisk_k_commitments, # [New in Whisk] ) - # Initialize all validators with predictable commitments - for val_index, pre_validator in enumerate(pre.validators): - whisk_commitment, whisk_tracker = get_initial_commitments(get_unique_whisk_k(post, ValidatorIndex(val_index))) - - 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, - whisk_commitment=whisk_commitment, - whisk_tracker=whisk_tracker, - ) - post.validators.append(post_validator) - # Do a candidate selection followed by a proposer selection so that we have proposers for the upcoming day # Use an old epoch when selecting candidates so that we don't get the same seed as in the next candidate selection - whisk_candidate_selection(post, epoch - WHISK_PROPOSER_SELECTION_GAP - 1) - whisk_proposer_selection(post, epoch) + select_whisk_candidate_trackers(post, Epoch(saturating_sub(epoch, WHISK_PROPOSER_SELECTION_GAP + 1))) + select_whisk_proposer_trackers(post, epoch) # Do a final round of candidate selection. # We need it so that we have something to shuffle over the upcoming shuffling phase. - whisk_candidate_selection(post, epoch) + select_whisk_candidate_trackers(post, epoch) return post ``` diff --git a/specs/altair/beacon-chain.md b/specs/altair/beacon-chain.md index 8c3a8877e8..1de39d6fc7 100644 --- a/specs/altair/beacon-chain.md +++ b/specs/altair/beacon-chain.md @@ -45,7 +45,7 @@ - [Modified `slash_validator`](#modified-slash_validator) - [Block processing](#block-processing) - [Modified `process_attestation`](#modified-process_attestation) - - [Modified `apply_deposit`](#modified-apply_deposit) + - [Modified `add_validator_to_registry`](#modified-add_validator_to_registry) - [Sync aggregate processing](#sync-aggregate-processing) - [Epoch processing](#epoch-processing) - [Justification and finalization](#justification-and-finalization) @@ -508,40 +508,23 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None: increase_balance(state, get_beacon_proposer_index(state), proposer_reward) ``` -#### Modified `apply_deposit` +#### Modified `add_validator_to_registry` -*Note*: The function `apply_deposit` is modified to initialize `inactivity_scores`, `previous_epoch_participation`, and `current_epoch_participation`. +*Note*: The function `add_validator_to_registry` is modified to initialize `inactivity_scores`, `previous_epoch_participation`, and `current_epoch_participation`. ```python -def apply_deposit(state: BeaconState, - pubkey: BLSPubkey, - withdrawal_credentials: Bytes32, - amount: uint64, - signature: BLSSignature) -> None: - validator_pubkeys = [validator.pubkey for validator in state.validators] - if pubkey not in validator_pubkeys: - # Verify the deposit signature (proof of possession) which is not checked by the deposit contract - deposit_message = DepositMessage( - pubkey=pubkey, - withdrawal_credentials=withdrawal_credentials, - amount=amount, - ) - domain = compute_domain(DOMAIN_DEPOSIT) # Fork-agnostic domain since deposits are valid across forks - signing_root = compute_signing_root(deposit_message, domain) - # Initialize validator if the deposit signature is valid - if bls.Verify(pubkey, signing_root, signature): - index = get_index_for_new_validator(state) - validator = get_validator_from_deposit(pubkey, withdrawal_credentials, amount) - set_or_append_list(state.validators, index, validator) - set_or_append_list(state.balances, index, amount) - # [New in Altair] - set_or_append_list(state.previous_epoch_participation, index, ParticipationFlags(0b0000_0000)) - set_or_append_list(state.current_epoch_participation, index, ParticipationFlags(0b0000_0000)) - set_or_append_list(state.inactivity_scores, index, uint64(0)) - else: - # Increase balance by deposit amount - index = ValidatorIndex(validator_pubkeys.index(pubkey)) - increase_balance(state, index, amount) +def add_validator_to_registry(state: BeaconState, + pubkey: BLSPubkey, + withdrawal_credentials: Bytes32, + amount: uint64) -> None: + index = get_index_for_new_validator(state) + validator = get_validator_from_deposit(pubkey, withdrawal_credentials, amount) + set_or_append_list(state.validators, index, validator) + set_or_append_list(state.balances, index, amount) + # [New in Altair] + set_or_append_list(state.previous_epoch_participation, index, ParticipationFlags(0b0000_0000)) + set_or_append_list(state.current_epoch_participation, index, ParticipationFlags(0b0000_0000)) + set_or_append_list(state.inactivity_scores, index, uint64(0)) ``` #### Sync aggregate processing diff --git a/specs/deneb/beacon-chain.md b/specs/deneb/beacon-chain.md index 148dc96d8f..321dfb25e3 100644 --- a/specs/deneb/beacon-chain.md +++ b/specs/deneb/beacon-chain.md @@ -24,16 +24,22 @@ - [Helper functions](#helper-functions) - [Misc](#misc) - [`kzg_commitment_to_versioned_hash`](#kzg_commitment_to_versioned_hash) + - [Beacon state accessors](#beacon-state-accessors) + - [Modified `get_attestation_participation_flag_indices`](#modified-get_attestation_participation_flag_indices) - [Beacon chain state transition function](#beacon-chain-state-transition-function) - [Execution engine](#execution-engine) - [Request data](#request-data) - [Modified `NewPayloadRequest`](#modified-newpayloadrequest) - [Engine APIs](#engine-apis) + - [`is_valid_block_hash`](#is_valid_block_hash) - [`is_valid_versioned_hashes`](#is_valid_versioned_hashes) + - [Modified `notify_new_payload`](#modified-notify_new_payload) - [Modified `verify_and_notify_new_payload`](#modified-verify_and_notify_new_payload) - [Block processing](#block-processing) + - [Modified `process_attestation`](#modified-process_attestation) - [Execution payload](#execution-payload) - - [`process_execution_payload`](#process_execution_payload) + - [Modified `process_execution_payload`](#modified-process_execution_payload) + - [Modified `process_voluntary_exit`](#modified-process_voluntary_exit) - [Testing](#testing) @@ -42,7 +48,10 @@ ## Introduction Deneb is a consensus-layer upgrade containing a number of features. Including: -* [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844): Shard Blob Transactions scale data-availability of Ethereum in a simple, forwards-compatible manner. +* [EIP-4788](https://eips.ethereum.org/EIPS/eip-4788): Beacon block root in the EVM +* [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844): Shard Blob Transactions scale data-availability of Ethereum in a simple, forwards-compatible manner +* [EIP-7044](https://github.com/ethereum/EIPs/pull/7044): Perpetually Valid Signed Voluntary Exits +* [EIP-7045](https://eips.ethereum.org/EIPS/eip-7045): Increase Max Attestation Inclusion Slot ## Custom types @@ -80,7 +89,6 @@ and are limited by `MAX_DATA_GAS_PER_BLOCK // DATA_GAS_PER_BLOB`. However the CL ## Configuration - ## Containers ### Extended containers @@ -168,6 +176,41 @@ def kzg_commitment_to_versioned_hash(kzg_commitment: KZGCommitment) -> Versioned return VERSIONED_HASH_VERSION_KZG + hash(kzg_commitment)[1:] ``` +### Beacon state accessors + +#### Modified `get_attestation_participation_flag_indices` + +*Note:* The function `get_attestation_participation_flag_indices` is modified to set the `TIMELY_TARGET_FLAG` for any correct target attestation, regardless of `inclusion_delay` as a baseline reward for any speed of inclusion of an attestation that contributes to justification of the contained chain for EIP-7045. + +```python +def get_attestation_participation_flag_indices(state: BeaconState, + data: AttestationData, + inclusion_delay: uint64) -> Sequence[int]: + """ + Return the flag indices that are satisfied by an attestation. + """ + if data.target.epoch == get_current_epoch(state): + justified_checkpoint = state.current_justified_checkpoint + else: + justified_checkpoint = state.previous_justified_checkpoint + + # Matching roots + is_matching_source = data.source == justified_checkpoint + is_matching_target = is_matching_source and data.target.root == get_block_root(state, data.target.epoch) + is_matching_head = is_matching_target and data.beacon_block_root == get_block_root_at_slot(state, data.slot) + assert is_matching_source + + participation_flag_indices = [] + if is_matching_source and inclusion_delay <= integer_squareroot(SLOTS_PER_EPOCH): + participation_flag_indices.append(TIMELY_SOURCE_FLAG_INDEX) + if is_matching_target: # [Modified in Deneb:EIP7045] + participation_flag_indices.append(TIMELY_TARGET_FLAG_INDEX) + if is_matching_head and inclusion_delay == MIN_ATTESTATION_INCLUSION_DELAY: + participation_flag_indices.append(TIMELY_HEAD_FLAG_INDEX) + + return participation_flag_indices +``` + ## Beacon chain state transition function ### Execution engine @@ -181,10 +224,25 @@ def kzg_commitment_to_versioned_hash(kzg_commitment: KZGCommitment) -> Versioned class NewPayloadRequest(object): execution_payload: ExecutionPayload versioned_hashes: Sequence[VersionedHash] + parent_beacon_block_root: Root ``` #### Engine APIs +##### `is_valid_block_hash` + +*Note*: The function `is_valid_block_hash` is modified to include the additional `parent_beacon_block_root` parameter for EIP-4788. + +```python +def is_valid_block_hash(self: ExecutionEngine, + execution_payload: ExecutionPayload, + parent_beacon_block_root: Root) -> bool: + """ + Return ``True`` if and only if ``execution_payload.block_hash`` is computed correctly. + """ + ... +``` + ##### `is_valid_versioned_hashes` ```python @@ -196,6 +254,20 @@ def is_valid_versioned_hashes(self: ExecutionEngine, new_payload_request: NewPay ... ``` +##### Modified `notify_new_payload` + +*Note*: The function `notify_new_payload` is modified to include the additional `parent_beacon_block_root` parameter for EIP-4788. + +```python +def notify_new_payload(self: ExecutionEngine, + execution_payload: ExecutionPayload, + parent_beacon_block_root: Root) -> bool: + """ + Return ``True`` if and only if ``execution_payload`` is valid with respect to ``self.execution_state``. + """ + ... +``` + ##### Modified `verify_and_notify_new_payload` ```python @@ -204,14 +276,19 @@ def verify_and_notify_new_payload(self: ExecutionEngine, """ Return ``True`` if and only if ``new_payload_request`` is valid with respect to ``self.execution_state``. """ - if not self.is_valid_block_hash(new_payload_request.execution_payload): + execution_payload = new_payload_request.execution_payload + parent_beacon_block_root = new_payload_request.parent_beacon_block_root + + # [Modified in Deneb:EIP4788] + if not self.is_valid_block_hash(execution_payload, parent_beacon_block_root): return False # [New in Deneb:EIP4844] if not self.is_valid_versioned_hashes(new_payload_request): return False - if not self.notify_new_payload(new_payload_request.execution_payload): + # [Modified in Deneb:EIP4788] + if not self.notify_new_payload(execution_payload, parent_beacon_block_root): return False return True @@ -219,9 +296,51 @@ def verify_and_notify_new_payload(self: ExecutionEngine, ### Block processing +#### Modified `process_attestation` + +*Note*: The function `process_attestation` is modified to expand valid slots for inclusion to those in both `target.epoch` epoch and `target.epoch + 1` epoch for EIP-7045. Additionally, it utilizes an updated version of `get_attestation_participation_flag_indices` to ensure rewards are available for the extended attestation inclusion range for EIP-7045. + +```python +def process_attestation(state: BeaconState, attestation: Attestation) -> None: + data = attestation.data + assert data.target.epoch in (get_previous_epoch(state), get_current_epoch(state)) + assert data.target.epoch == compute_epoch_at_slot(data.slot) + assert data.slot + MIN_ATTESTATION_INCLUSION_DELAY <= state.slot # [Modified in Deneb:EIP7045] + assert data.index < get_committee_count_per_slot(state, data.target.epoch) + + committee = get_beacon_committee(state, data.slot, data.index) + assert len(attestation.aggregation_bits) == len(committee) + + # Participation flag indices + participation_flag_indices = get_attestation_participation_flag_indices(state, data, state.slot - data.slot) + + # Verify signature + assert is_valid_indexed_attestation(state, get_indexed_attestation(state, attestation)) + + # Update epoch participation flags + if data.target.epoch == get_current_epoch(state): + epoch_participation = state.current_epoch_participation + else: + epoch_participation = state.previous_epoch_participation + + proposer_reward_numerator = 0 + for index in get_attesting_indices(state, data, attestation.aggregation_bits): + for flag_index, weight in enumerate(PARTICIPATION_FLAG_WEIGHTS): + if flag_index in participation_flag_indices and not has_flag(epoch_participation[index], flag_index): + epoch_participation[index] = add_flag(epoch_participation[index], flag_index) + proposer_reward_numerator += get_base_reward(state, index) * weight + + # Reward proposer + proposer_reward_denominator = (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT) * WEIGHT_DENOMINATOR // PROPOSER_WEIGHT + proposer_reward = Gwei(proposer_reward_numerator // proposer_reward_denominator) + increase_balance(state, get_beacon_proposer_index(state), proposer_reward) +``` + #### Execution payload -##### `process_execution_payload` +##### Modified `process_execution_payload` + +*Note*: The function `process_execution_payload` is modified to pass `versioned_hashes` into `execution_engine.verify_and_notify_new_payload` and to assign the new fields in `ExecutionPayloadHeader` for EIP-4844. It is also modified to pass in the parent beacon block root to support EIP-4788. ```python def process_execution_payload(state: BeaconState, body: BeaconBlockBody, execution_engine: ExecutionEngine) -> None: @@ -239,9 +358,14 @@ def process_execution_payload(state: BeaconState, body: BeaconBlockBody, executi # Verify the execution payload is valid # [Modified in Deneb:EIP4844] Pass `versioned_hashes` to Execution Engine + # [Modified in Deneb:EIP4788] Pass `parent_beacon_block_root` to Execution Engine versioned_hashes = [kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments] assert execution_engine.verify_and_notify_new_payload( - NewPayloadRequest(execution_payload=payload, versioned_hashes=versioned_hashes) + NewPayloadRequest( + execution_payload=payload, + versioned_hashes=versioned_hashes, + parent_beacon_block_root=state.latest_block_header.parent_root, + ) ) # Cache execution payload header @@ -266,6 +390,31 @@ def process_execution_payload(state: BeaconState, body: BeaconBlockBody, executi ) ``` +#### Modified `process_voluntary_exit` + +*Note*: The function `process_voluntary_exit` is modified to use the a fixed fork version -- `CAPELLA_FORK_VERSION` -- for EIP-7044. + +```python +def process_voluntary_exit(state: BeaconState, signed_voluntary_exit: SignedVoluntaryExit) -> None: + voluntary_exit = signed_voluntary_exit.message + validator = state.validators[voluntary_exit.validator_index] + # Verify the validator is active + assert is_active_validator(validator, get_current_epoch(state)) + # Verify exit has not been initiated + assert validator.exit_epoch == FAR_FUTURE_EPOCH + # Exits must specify an epoch when they become valid; they are not valid before then + assert get_current_epoch(state) >= voluntary_exit.epoch + # Verify the validator has been active long enough + assert get_current_epoch(state) >= validator.activation_epoch + SHARD_COMMITTEE_PERIOD + # Verify signature + # [Modified in Deneb:EIP7044] + domain = compute_domain(DOMAIN_VOLUNTARY_EXIT, CAPELLA_FORK_VERSION, state.genesis_validators_root) + signing_root = compute_signing_root(voluntary_exit, domain) + assert bls.Verify(validator.pubkey, signing_root, signed_voluntary_exit.signature) + # Initiate exit + initiate_validator_exit(state, voluntary_exit.validator_index) +``` + ## Testing *Note*: The function `initialize_beacon_state_from_eth1` is modified for pure Deneb testing only. diff --git a/specs/deneb/fork-choice.md b/specs/deneb/fork-choice.md index bbc7fa0f89..23eef436c1 100644 --- a/specs/deneb/fork-choice.md +++ b/specs/deneb/fork-choice.md @@ -8,7 +8,8 @@ - [Introduction](#introduction) - [Containers](#containers) - [Helpers](#helpers) - - [`is_data_available`](#is_data_available) + - [Extended `PayloadAttributes`](#extended-payloadattributes) + - [`is_data_available`](#is_data_available) - [Updated fork-choice handlers](#updated-fork-choice-handlers) - [`on_block`](#on_block) @@ -23,7 +24,21 @@ This is the modification of the fork choice accompanying the Deneb upgrade. ## Helpers -#### `is_data_available` +### Extended `PayloadAttributes` + +`PayloadAttributes` is extended with the parent beacon block root for EIP-4788. + +```python +@dataclass +class PayloadAttributes(object): + timestamp: uint64 + prev_randao: Bytes32 + suggested_fee_recipient: ExecutionAddress + withdrawals: Sequence[Withdrawal] + parent_beacon_block_root: Root # [New in Deneb:EIP4788] +``` + +### `is_data_available` *[New in Deneb:EIP4844]* diff --git a/specs/deneb/fork.md b/specs/deneb/fork.md index ffced6a59a..08af2fd351 100644 --- a/specs/deneb/fork.md +++ b/specs/deneb/fork.md @@ -57,7 +57,7 @@ def compute_fork_version(epoch: Epoch) -> Version: ### Fork trigger -TBD. This fork is defined for testing purposes, the EIP may be combined with other consensus-layer upgrade. +TBD. This fork is defined for testing purposes. For now, we assume the condition will be triggered at epoch `DENEB_FORK_EPOCH`. Note that for the pure Deneb networks, we don't apply `upgrade_to_deneb` since it starts with Deneb version logic. diff --git a/specs/deneb/p2p-interface.md b/specs/deneb/p2p-interface.md index 809e405a66..f857ffdf13 100644 --- a/specs/deneb/p2p-interface.md +++ b/specs/deneb/p2p-interface.md @@ -23,6 +23,9 @@ The specification of these changes continues in the same format as the network s - [Global topics](#global-topics) - [`beacon_block`](#beacon_block) - [`blob_sidecar_{subnet_id}`](#blob_sidecar_subnet_id) + - [`beacon_aggregate_and_proof`](#beacon_aggregate_and_proof) + - [Attestation subnets](#attestation-subnets) + - [`beacon_attestation_{subnet_id}`](#beacon_attestation_subnet_id) - [Transitioning the gossip](#transitioning-the-gossip) - [The Req/Resp domain](#the-reqresp-domain) - [Messages](#messages) @@ -106,7 +109,11 @@ Some gossip meshes are upgraded in the fork of Deneb to support upgraded types. Topics follow the same specification as in prior upgrades. -The `beacon_block` topic is modified to also support deneb blocks and new topics are added per table below. All other topics remain stable. +The `beacon_block` topic is modified to also support Deneb blocks and new topics are added per table below. + +The `voluntary_exit` topic is implicitly modified due to the lock-in use of `CAPELLA_FORK_VERSION` for this message signature validation for EIP-7044. + +The `beacon_aggregate_and_proof` and `beacon_attestation_{subnet_id}` topics are modified to support the gossip of attestations created in epoch `N` to be gossiped through the entire range of slots in epoch `N+1` rather than only through one epoch of slots for EIP-7045. The specification around the creation, validation, and dissemination of messages has not changed from the Capella document unless explicitly noted here. @@ -124,7 +131,9 @@ Deneb introduces new global topics for blob sidecars. ###### `beacon_block` -The *type* of the payload of this topic changes to the (modified) `SignedBeaconBlock` found in deneb. +The *type* of the payload of this topic changes to the (modified) `SignedBeaconBlock` found in Deneb. + +*[Modified in Deneb:EIP4844]* New validation: @@ -150,6 +159,41 @@ The following validations MUST pass before forwarding the `signed_blob_sidecar` - _[REJECT]_ The sidecar is proposed by the expected `proposer_index` for the block's slot in the context of the current shuffling (defined by `block_parent_root`/`slot`). If the `proposer_index` cannot immediately be verified against the expected shuffling, the sidecar MAY be queued for later processing while proposers for the block's branch are calculated -- in such a case _do not_ `REJECT`, instead `IGNORE` this message. +###### `beacon_aggregate_and_proof` + +*[Modified in Deneb:EIP7045]* + +The following validation is removed: +* _[IGNORE]_ `aggregate.data.slot` is within the last `ATTESTATION_PROPAGATION_SLOT_RANGE` slots (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) -- + i.e. `aggregate.data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot >= aggregate.data.slot` + (a client MAY queue future aggregates for processing at the appropriate slot). + +The following validations are added in its place: +* _[IGNORE]_ `aggregate.data.slot` is equal to or earlier than the `current_slot` (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) -- + i.e. `aggregate.data.slot <= current_slot` + (a client MAY queue future aggregates for processing at the appropriate slot). +* _[IGNORE]_ the epoch of `aggregate.data.slot` is either the current or previous epoch + (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) -- + i.e. `compute_epoch_at_slot(aggregate.data.slot) in (get_previous_epoch(state), get_current_epoch(state))` + +##### Attestation subnets + +###### `beacon_attestation_{subnet_id}` + +*[Modified in Deneb:EIP7045]* + +The following validation is removed: +* _[IGNORE]_ `attestation.data.slot` is within the last `ATTESTATION_PROPAGATION_SLOT_RANGE` slots (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) -- + i.e. `attestation.data.slot + ATTESTATION_PROPAGATION_SLOT_RANGE >= current_slot >= attestation.data.slot` + (a client MAY queue future attestations for processing at the appropriate slot). + +The following validations are added in its place: +* _[IGNORE]_ `attestation.data.slot` is equal to or earlier than the `current_slot` (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) -- + i.e. `attestation.data.slot <= current_slot` + (a client MAY queue future attestation for processing at the appropriate slot). +* _[IGNORE]_ the epoch of `attestation.data.slot` is either the current or previous epoch + (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) -- + i.e. `compute_epoch_at_slot(attestation.data.slot) in (get_previous_epoch(state), get_current_epoch(state))` #### Transitioning the gossip diff --git a/specs/deneb/validator.md b/specs/deneb/validator.md index 3157ccf218..7b8a81b19a 100644 --- a/specs/deneb/validator.md +++ b/specs/deneb/validator.md @@ -19,6 +19,7 @@ - [Beacon chain responsibilities](#beacon-chain-responsibilities) - [Block and sidecar proposal](#block-and-sidecar-proposal) - [Constructing the `BeaconBlockBody`](#constructing-the-beaconblockbody) + - [ExecutionPayload](#executionpayload) - [Blob KZG commitments](#blob-kzg-commitments) - [Constructing the `SignedBlobSidecar`s](#constructing-the-signedblobsidecars) - [Sidecar](#sidecar) @@ -88,11 +89,46 @@ All validator responsibilities remain unchanged other than those noted below. #### Constructing the `BeaconBlockBody` +##### ExecutionPayload + +`prepare_execution_payload` is updated from the Capella specs to provide the parent beacon block root. + +*Note*: In this section, `state` is the state of the slot for the block proposal _without_ the block yet applied. +That is, `state` is the `previous_state` processed through any empty slots up to the assigned slot using `process_slots(previous_state, slot)`. + +*Note*: The only change made to `prepare_execution_payload` is to add the parent beacon block root as an additional +parameter to the `PayloadAttributes`. + +```python +def prepare_execution_payload(state: BeaconState, + safe_block_hash: Hash32, + finalized_block_hash: Hash32, + suggested_fee_recipient: ExecutionAddress, + execution_engine: ExecutionEngine) -> Optional[PayloadId]: + # Verify consistency of the parent hash with respect to the previous execution payload header + 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), + parent_beacon_block_root=hash_tree_root(state.latest_block_header), # [New in Deneb:EIP4788] + ) + 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, + ) +``` + ##### Blob KZG commitments *[New in Deneb:EIP4844]* -1. After retrieving the execution payload from the execution engine as specified in Capella, +1. After retrieving the execution payload from the execution engine as specified above, use the `payload_id` to retrieve `blobs`, `blob_kzg_commitments`, and `blob_kzg_proofs` via `get_payload(payload_id).blobs_bundle`. 2. Set `block.body.blob_kzg_commitments = blob_kzg_commitments`. diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index b77e017ab7..bdfb078380 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -59,6 +59,7 @@ - [`xor`](#xor) - [`uint_to_bytes`](#uint_to_bytes) - [`bytes_to_uint64`](#bytes_to_uint64) + - [`saturating_sub`](#saturating_sub) - [Crypto](#crypto) - [`hash`](#hash) - [`hash_tree_root`](#hash_tree_root) @@ -630,6 +631,16 @@ def bytes_to_uint64(data: bytes) -> uint64: return uint64(int.from_bytes(data, ENDIANNESS)) ``` +#### `saturating_sub` + +```python +def saturating_sub(a: int, b: int) -> int: + """ + Computes a - b, saturating at numeric bounds. + """ + return a - b if a > b else 0 +``` + ### Crypto #### `hash` @@ -1849,6 +1860,15 @@ def get_validator_from_deposit(pubkey: BLSPubkey, withdrawal_credentials: Bytes3 ) ``` +```python +def add_validator_to_registry(state: BeaconState, + pubkey: BLSPubkey, + withdrawal_credentials: Bytes32, + amount: uint64) -> None: + state.validators.append(get_validator_from_deposit(pubkey, withdrawal_credentials, amount)) + state.balances.append(amount) +``` + ```python def apply_deposit(state: BeaconState, pubkey: BLSPubkey, @@ -1865,12 +1885,8 @@ def apply_deposit(state: BeaconState, ) domain = compute_domain(DOMAIN_DEPOSIT) # Fork-agnostic domain since deposits are valid across forks signing_root = compute_signing_root(deposit_message, domain) - if not bls.Verify(pubkey, signing_root, signature): - return - - # Add validator and balance entries - state.validators.append(get_validator_from_deposit(pubkey, withdrawal_credentials, amount)) - state.balances.append(amount) + if bls.Verify(pubkey, signing_root, signature): + add_validator_to_registry(state, pubkey, withdrawal_credentials, amount) else: # Increase balance by deposit amount index = ValidatorIndex(validator_pubkeys.index(pubkey)) diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index c0d18b08f3..bbb4c4d427 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -55,7 +55,7 @@ It consists of four main sections: - [ENR structure](#enr-structure) - [Attestation subnet bitfield](#attestation-subnet-bitfield) - [`eth2` field](#eth2-field) - - [Attestation subnet subcription](#attestation-subnet-subcription) + - [Attestation subnet subscription](#attestation-subnet-subscription) - [Design decision rationale](#design-decision-rationale) - [Transport](#transport-1) - [Why are we defining specific transports?](#why-are-we-defining-specific-transports) @@ -1002,7 +1002,7 @@ Clients MAY connect to peers with the same `fork_digest` but a different `next_f Unless `ENRForkID` is manually updated to matching prior to the earlier `next_fork_epoch` of the two clients, these connecting clients will be unable to successfully interact starting at the earlier `next_fork_epoch`. -### Attestation subnet subcription +### Attestation subnet subscription Because Phase 0 does not have shards and thus does not have Shard Committees, there is no stable backbone to the attestation subnets (`beacon_attestation_{subnet_id}`). To provide this stability, each beacon node should: diff --git a/tests/core/pyspec/eth2spec/VERSION.txt b/tests/core/pyspec/eth2spec/VERSION.txt index 45eafc27d4..c431216bf2 100644 --- a/tests/core/pyspec/eth2spec/VERSION.txt +++ b/tests/core/pyspec/eth2spec/VERSION.txt @@ -1 +1 @@ -1.4.0-alpha.3 +1.4.0-beta.0 diff --git a/tests/core/pyspec/eth2spec/test/bellatrix/block_processing/test_process_voluntary_exit.py b/tests/core/pyspec/eth2spec/test/bellatrix/block_processing/test_process_voluntary_exit.py index f4fcaac689..ea3b57a97a 100644 --- a/tests/core/pyspec/eth2spec/test/bellatrix/block_processing/test_process_voluntary_exit.py +++ b/tests/core/pyspec/eth2spec/test/bellatrix/block_processing/test_process_voluntary_exit.py @@ -2,6 +2,11 @@ spec_state_test, always_bls, with_bellatrix_and_later, + with_phases, +) +from eth2spec.test.helpers.constants import ( + BELLATRIX, + CAPELLA, ) from eth2spec.test.helpers.keys import pubkey_to_privkey from eth2spec.test.helpers.state import ( @@ -12,8 +17,10 @@ sign_voluntary_exit, ) +BELLATRIX_AND_CAPELLA = [BELLATRIX, CAPELLA] + -def _run_voluntary_exit_processing_test( +def run_voluntary_exit_processing_test( spec, state, fork_version, @@ -51,7 +58,7 @@ def _run_voluntary_exit_processing_test( @spec_state_test @always_bls def test_invalid_voluntary_exit_with_current_fork_version_is_before_fork_epoch(spec, state): - yield from _run_voluntary_exit_processing_test( + yield from run_voluntary_exit_processing_test( spec, state, fork_version=state.fork.current_version, @@ -60,11 +67,11 @@ def test_invalid_voluntary_exit_with_current_fork_version_is_before_fork_epoch(s ) -@with_bellatrix_and_later +@with_phases(BELLATRIX_AND_CAPELLA) @spec_state_test @always_bls def test_voluntary_exit_with_current_fork_version_not_is_before_fork_epoch(spec, state): - yield from _run_voluntary_exit_processing_test( + yield from run_voluntary_exit_processing_test( spec, state, fork_version=state.fork.current_version, @@ -72,13 +79,13 @@ def test_voluntary_exit_with_current_fork_version_not_is_before_fork_epoch(spec, ) -@with_bellatrix_and_later +@with_phases([BELLATRIX, CAPELLA]) @spec_state_test @always_bls def test_voluntary_exit_with_previous_fork_version_is_before_fork_epoch(spec, state): assert state.fork.previous_version != state.fork.current_version - yield from _run_voluntary_exit_processing_test( + yield from run_voluntary_exit_processing_test( spec, state, fork_version=state.fork.previous_version, @@ -86,13 +93,13 @@ def test_voluntary_exit_with_previous_fork_version_is_before_fork_epoch(spec, st ) -@with_bellatrix_and_later +@with_phases(BELLATRIX_AND_CAPELLA) @spec_state_test @always_bls def test_invalid_voluntary_exit_with_previous_fork_version_not_is_before_fork_epoch(spec, state): assert state.fork.previous_version != state.fork.current_version - yield from _run_voluntary_exit_processing_test( + yield from run_voluntary_exit_processing_test( spec, state, fork_version=state.fork.previous_version, @@ -107,7 +114,7 @@ def test_invalid_voluntary_exit_with_previous_fork_version_not_is_before_fork_ep def test_invalid_voluntary_exit_with_genesis_fork_version_is_before_fork_epoch(spec, state): assert spec.config.GENESIS_FORK_VERSION not in (state.fork.previous_version, state.fork.current_version) - yield from _run_voluntary_exit_processing_test( + yield from run_voluntary_exit_processing_test( spec, state, fork_version=spec.config.GENESIS_FORK_VERSION, @@ -122,7 +129,7 @@ def test_invalid_voluntary_exit_with_genesis_fork_version_is_before_fork_epoch(s def test_invalid_voluntary_exit_with_genesis_fork_version_not_is_before_fork_epoch(spec, state): assert spec.config.GENESIS_FORK_VERSION not in (state.fork.previous_version, state.fork.current_version) - yield from _run_voluntary_exit_processing_test( + yield from run_voluntary_exit_processing_test( spec, state, fork_version=spec.config.GENESIS_FORK_VERSION, diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 00589c8721..48f6857f64 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -534,12 +534,13 @@ def wrapper(*args, spec: Spec, **kw): return decorator +with_light_client = with_phases(LIGHT_CLIENT_TESTING_FORKS) + with_altair_and_later = with_all_phases_from(ALTAIR) with_bellatrix_and_later = with_all_phases_from(BELLATRIX) with_capella_and_later = with_all_phases_from(CAPELLA) with_deneb_and_later = with_all_phases_from(DENEB) with_eip6110_and_later = with_all_phases_from(EIP6110) -with_light_client = with_phases(LIGHT_CLIENT_TESTING_FORKS) class quoted_str(str): diff --git a/tests/core/pyspec/eth2spec/test/deneb/block_processing/test_process_voluntary_exit.py b/tests/core/pyspec/eth2spec/test/deneb/block_processing/test_process_voluntary_exit.py new file mode 100644 index 0000000000..b01eaab0e1 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/deneb/block_processing/test_process_voluntary_exit.py @@ -0,0 +1,88 @@ +from eth2spec.test.context import ( + always_bls, + spec_state_test, + with_deneb_and_later, +) +from eth2spec.test.helpers.constants import ( + DENEB, +) +from eth2spec.test.bellatrix.block_processing.test_process_voluntary_exit import ( + run_voluntary_exit_processing_test, +) + + +@with_deneb_and_later +@spec_state_test +@always_bls +def test_invalid_voluntary_exit_with_current_fork_version_not_is_before_fork_epoch(spec, state): + """ + Since Deneb, the VoluntaryExit domain is fixed to `CAPELLA_FORK_VERSION` + """ + assert state.fork.current_version != spec.config.CAPELLA_FORK_VERSION + yield from run_voluntary_exit_processing_test( + spec, + state, + fork_version=state.fork.current_version, + is_before_fork_epoch=False, + valid=False, + ) + + +@with_deneb_and_later +@spec_state_test +@always_bls +def test_voluntary_exit_with_previous_fork_version_not_is_before_fork_epoch(spec, state): + """ + Since Deneb, the VoluntaryExit domain is fixed to `CAPELLA_FORK_VERSION` + + Note: This test is valid for ``spec.fork == DENEB`` and invalid for subsequent forks + """ + assert state.fork.previous_version != state.fork.current_version + + if spec.fork == DENEB: + assert state.fork.previous_version == spec.config.CAPELLA_FORK_VERSION + yield from run_voluntary_exit_processing_test( + spec, + state, + fork_version=state.fork.previous_version, + is_before_fork_epoch=False, + ) + else: + assert state.fork.previous_version != spec.config.CAPELLA_FORK_VERSION + yield from run_voluntary_exit_processing_test( + spec, + state, + fork_version=state.fork.previous_version, + is_before_fork_epoch=False, + valid=False, + ) + + +@with_deneb_and_later +@spec_state_test +@always_bls +def test_voluntary_exit_with_previous_fork_version_is_before_fork_epoch(spec, state): + """ + Since Deneb, the VoluntaryExit domain is fixed to `CAPELLA_FORK_VERSION` + + Note: This test is valid for ``spec.fork == DENEB`` and invalid for subsequent forks + """ + assert state.fork.previous_version != state.fork.current_version + + if spec.fork == DENEB: + assert state.fork.previous_version == spec.config.CAPELLA_FORK_VERSION + yield from run_voluntary_exit_processing_test( + spec, + state, + fork_version=state.fork.previous_version, + is_before_fork_epoch=True, + ) + else: + assert state.fork.previous_version != spec.config.CAPELLA_FORK_VERSION + yield from run_voluntary_exit_processing_test( + spec, + state, + fork_version=state.fork.previous_version, + is_before_fork_epoch=True, + valid=False, + ) diff --git a/tests/core/pyspec/eth2spec/test/deneb/sanity/test_blocks.py b/tests/core/pyspec/eth2spec/test/deneb/sanity/test_blocks.py index 2af330efba..c64efe747b 100644 --- a/tests/core/pyspec/eth2spec/test/deneb/sanity/test_blocks.py +++ b/tests/core/pyspec/eth2spec/test/deneb/sanity/test_blocks.py @@ -1,16 +1,24 @@ from eth2spec.test.helpers.state import ( - state_transition_and_sign_block + state_transition_and_sign_block, + next_epoch_via_block, + transition_to, ) from eth2spec.test.helpers.block import ( - build_empty_block_for_next_slot + build_empty_block_for_next_slot, ) from eth2spec.test.context import ( + DENEB, spec_state_test, + spec_configured_state_test, with_deneb_and_later, + with_phases, ) from eth2spec.test.helpers.execution_payload import ( compute_el_block_hash, ) +from eth2spec.test.helpers.attestations import ( + get_valid_attestation, +) from eth2spec.test.helpers.sharding import ( get_sample_opaque_tx, ) @@ -58,3 +66,35 @@ def test_max_blobs_per_block(spec, state): @spec_state_test def test_invalid_exceed_max_blobs_per_block(spec, state): yield from run_block_with_blobs(spec, state, blob_count=spec.MAX_BLOBS_PER_BLOCK + 1, valid=False) + + +@with_phases([DENEB]) +@spec_configured_state_test({ + 'DENEB_FORK_EPOCH': 2, +}) +def test_include_attestation_from_previous_fork_with_new_range(spec, state): + # Transition to the epoch prior to the fork epoch + next_epoch_via_block(spec, state) + + # Generate an attestation for slot 0 of this epoch + attestation = get_valid_attestation(spec, state, signed=True) + + # Transition to second to last slot in `DENEB_FORK_EPOCH` + next_epoch_via_block(spec, state) + current_epoch = spec.get_current_epoch(state) + assert current_epoch == spec.config.DENEB_FORK_EPOCH + penultimate_slot = spec.compute_start_slot_at_epoch(current_epoch + 1) - 2 + transition_to(spec, state, penultimate_slot) + + # Ensure the new state is in the increased EIP-7045 slot inclusion range + assert penultimate_slot - attestation.data.slot > spec.SLOTS_PER_EPOCH + + block = build_empty_block_for_next_slot(spec, state) + block.body.attestations.append(attestation) + + yield 'pre', state + + signed_block = state_transition_and_sign_block(spec, state, block) + + yield 'blocks', [signed_block] + yield 'post', state diff --git a/tests/core/pyspec/eth2spec/test/helpers/attestations.py b/tests/core/pyspec/eth2spec/test/helpers/attestations.py index 360e194f59..4899e62243 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/attestations.py +++ b/tests/core/pyspec/eth2spec/test/helpers/attestations.py @@ -5,7 +5,7 @@ from eth2spec.test.context import expect_assertion_error from eth2spec.test.helpers.state import state_transition_and_sign_block, next_epoch, next_slot from eth2spec.test.helpers.block import build_empty_block_for_next_slot -from eth2spec.test.helpers.forks import is_post_altair +from eth2spec.test.helpers.forks import is_post_altair, is_post_deneb from eth2spec.test.helpers.keys import privkeys from eth2spec.utils import bls from eth2spec.utils.ssz.ssz_typing import Bitlist @@ -158,6 +158,14 @@ def get_attestation_signature(spec, state, attestation_data, privkey): return bls.Sign(privkey, signing_root) +def compute_max_inclusion_slot(spec, attestation): + if is_post_deneb(spec): + next_epoch = spec.compute_epoch_at_slot(attestation.data.slot) + 1 + end_of_next_epoch = spec.compute_start_slot_at_epoch(next_epoch + 1) - 1 + return end_of_next_epoch + return attestation.data.slot + spec.SLOTS_PER_EPOCH + + def fill_aggregate_attestation(spec, state, attestation, signed=False, filter_participant_set=None): """ `signed`: Signing is optional. diff --git a/tests/core/pyspec/eth2spec/test/helpers/constants.py b/tests/core/pyspec/eth2spec/test/helpers/constants.py index 601650e97f..049c354caf 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/constants.py +++ b/tests/core/pyspec/eth2spec/test/helpers/constants.py @@ -30,7 +30,7 @@ # Formal forks *MAINNET_FORKS, DENEB, - # Experimental features + # Experimental patches EIP6110, ) # The forks that have light client specs diff --git a/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py b/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py index cac101dff1..2e8139db64 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py +++ b/tests/core/pyspec/eth2spec/test/helpers/voluntary_exits.py @@ -1,29 +1,31 @@ from random import Random from eth2spec.utils import bls from eth2spec.test.context import expect_assertion_error +from eth2spec.test.helpers.forks import is_post_deneb from eth2spec.test.helpers.keys import privkeys def prepare_signed_exits(spec, state, indices, fork_version=None): - if fork_version is None: - domain = spec.get_domain(state, spec.DOMAIN_VOLUNTARY_EXIT) - else: - domain = spec.compute_domain(spec.DOMAIN_VOLUNTARY_EXIT, fork_version, state.genesis_validators_root) - def create_signed_exit(index): - exit = spec.VoluntaryExit( + voluntary_exit = spec.VoluntaryExit( epoch=spec.get_current_epoch(state), validator_index=index, ) - signing_root = spec.compute_signing_root(exit, domain) - return spec.SignedVoluntaryExit(message=exit, signature=bls.Sign(privkeys[index], signing_root)) + return sign_voluntary_exit(spec, state, voluntary_exit, privkeys[index], fork_version=fork_version) return [create_signed_exit(index) for index in indices] def sign_voluntary_exit(spec, state, voluntary_exit, privkey, fork_version=None): if fork_version is None: - domain = spec.get_domain(state, spec.DOMAIN_VOLUNTARY_EXIT, voluntary_exit.epoch) + if is_post_deneb(spec): + domain = spec.compute_domain( + spec.DOMAIN_VOLUNTARY_EXIT, + spec.config.CAPELLA_FORK_VERSION, + state.genesis_validators_root, + ) + else: + domain = spec.get_domain(state, spec.DOMAIN_VOLUNTARY_EXIT, voluntary_exit.epoch) else: domain = spec.compute_domain(spec.DOMAIN_VOLUNTARY_EXIT, fork_version, state.genesis_validators_root) diff --git a/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_attestation.py b/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_attestation.py index 7595ce9cbe..94a5481fab 100644 --- a/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_attestation.py +++ b/tests/core/pyspec/eth2spec/test/phase0/block_processing/test_process_attestation.py @@ -12,6 +12,7 @@ get_valid_attestation, sign_aggregate_attestation, sign_attestation, + compute_max_inclusion_slot, ) from eth2spec.test.helpers.state import ( next_slots, @@ -95,11 +96,22 @@ def test_invalid_before_inclusion_delay(spec, state): @with_all_phases @spec_state_test -def test_invalid_after_epoch_slots(spec, state): +def test_at_max_inclusion_slot(spec, state): attestation = get_valid_attestation(spec, state, signed=True) # increment past latest inclusion slot - transition_to_slot_via_block(spec, state, state.slot + spec.SLOTS_PER_EPOCH + 1) + transition_to_slot_via_block(spec, state, compute_max_inclusion_slot(spec, attestation)) + + yield from run_attestation_processing(spec, state, attestation) + + +@with_all_phases +@spec_state_test +def test_invalid_after_max_inclusion_slot(spec, state): + attestation = get_valid_attestation(spec, state, signed=True) + + # increment past latest inclusion slot + transition_to_slot_via_block(spec, state, compute_max_inclusion_slot(spec, attestation) + 1) yield from run_attestation_processing(spec, state, attestation, valid=False) @@ -361,7 +373,7 @@ def test_invalid_too_few_aggregation_bits(spec, state): # -# Full correct atttestation contents at different slot inclusions +# Full correct attestation contents at different slot inclusions # @with_all_phases @@ -393,11 +405,20 @@ def test_correct_attestation_included_at_one_epoch_delay(spec, state): @with_all_phases @spec_state_test -def test_invalid_correct_attestation_included_after_epoch_delay(spec, state): +def test_correct_attestation_included_at_max_inclusion_slot(spec, state): + attestation = get_valid_attestation(spec, state, signed=True) + next_slots(spec, state, compute_max_inclusion_slot(spec, attestation)) + + yield from run_attestation_processing(spec, state, attestation) + + +@with_all_phases +@spec_state_test +def test_invalid_correct_attestation_included_after_max_inclusion_slot(spec, state): attestation = get_valid_attestation(spec, state, signed=True) # increment past latest inclusion slot - next_slots(spec, state, spec.SLOTS_PER_EPOCH + 1) + next_slots(spec, state, compute_max_inclusion_slot(spec, attestation) + 1) yield from run_attestation_processing(spec, state, attestation, valid=False) @@ -432,9 +453,9 @@ def test_incorrect_head_included_at_sqrt_epoch_delay(spec, state): @with_all_phases @spec_state_test -def test_incorrect_head_included_at_epoch_delay(spec, state): +def test_incorrect_head_included_at_max_inclusion_slot(spec, state): attestation = get_valid_attestation(spec, state, signed=False) - next_slots(spec, state, spec.SLOTS_PER_EPOCH) + next_slots(spec, state, compute_max_inclusion_slot(spec, attestation)) attestation.data.beacon_block_root = b'\x42' * 32 sign_attestation(spec, state, attestation) @@ -444,11 +465,11 @@ def test_incorrect_head_included_at_epoch_delay(spec, state): @with_all_phases @spec_state_test -def test_invalid_incorrect_head_included_after_epoch_delay(spec, state): +def test_invalid_incorrect_head_included_after_max_inclusion_slot(spec, state): attestation = get_valid_attestation(spec, state, signed=False) # increment past latest inclusion slot - next_slots(spec, state, spec.SLOTS_PER_EPOCH + 1) + next_slots(spec, state, compute_max_inclusion_slot(spec, attestation) + 1) attestation.data.beacon_block_root = b'\x42' * 32 sign_attestation(spec, state, attestation) @@ -501,10 +522,10 @@ def test_incorrect_head_and_target_included_at_epoch_delay(spec, state): @with_all_phases @spec_state_test -def test_invalid_incorrect_head_and_target_included_after_epoch_delay(spec, state): +def test_invalid_incorrect_head_and_target_included_after_max_inclusion_slot(spec, state): attestation = get_valid_attestation(spec, state, signed=False) # increment past latest inclusion slot - next_slots(spec, state, spec.SLOTS_PER_EPOCH + 1) + next_slots(spec, state, compute_max_inclusion_slot(spec, attestation) + 1) attestation.data.beacon_block_root = b'\x42' * 32 attestation.data.target.root = b'\x42' * 32 @@ -555,10 +576,10 @@ def test_incorrect_target_included_at_epoch_delay(spec, state): @with_all_phases @spec_state_test -def test_invalid_incorrect_target_included_after_epoch_delay(spec, state): +def test_invalid_incorrect_target_included_after_max_inclusion_slot(spec, state): attestation = get_valid_attestation(spec, state, signed=False) # increment past latest inclusion slot - next_slots(spec, state, spec.SLOTS_PER_EPOCH + 1) + next_slots(spec, state, compute_max_inclusion_slot(spec, attestation) + 1) attestation.data.target.root = b'\x42' * 32 sign_attestation(spec, state, attestation) diff --git a/tests/generators/operations/main.py b/tests/generators/operations/main.py index 7aea6fc6f4..053236c8d7 100644 --- a/tests/generators/operations/main.py +++ b/tests/generators/operations/main.py @@ -39,6 +39,7 @@ _new_deneb_mods = {key: 'eth2spec.test.deneb.block_processing.test_process_' + key for key in [ 'execution_payload', + 'voluntary_exit', ]} deneb_mods = combine_mods(_new_deneb_mods, capella_mods)