diff --git a/.gitignore b/.gitignore index 56f84dafd8..1cbca42870 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ tests/core/pyspec/eth2spec/fulu/ tests/core/pyspec/eth2spec/whisk/ tests/core/pyspec/eth2spec/eip6800/ tests/core/pyspec/eth2spec/eip7732/ +tests/core/pyspec/eth2spec/eip7805/ # coverage reports .htmlcov diff --git a/Makefile b/Makefile index 09e914c3ca..fe40cf9f34 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,8 @@ ALL_EXECUTABLE_SPEC_NAMES = \ fulu \ whisk \ eip6800 \ - eip7732 + eip7732 \ + eip7805 # A list of fake targets. .PHONY: \ @@ -211,7 +212,7 @@ _check_toc: $(MARKDOWN_FILES:=.toc) @[ "$$(find . -name '*.md.tmp' -print -quit)" ] && exit 1 || exit 0 # Check for mistakes. -lint: $(ETH2SPEC) pyspec _check_toc +lint: eth2spec pyspec _check_toc @$(CODESPELL_VENV) . --skip "./.git,$(VENV),$(PYSPEC_DIR)/.mypy_cache" -I .codespell-whitelist @$(PYTHON_VENV) -m flake8 --config $(FLAKE8_CONFIG) $(PYSPEC_DIR)/eth2spec @$(PYTHON_VENV) -m flake8 --config $(FLAKE8_CONFIG) $(TEST_GENERATORS_DIR) @@ -275,4 +276,4 @@ kzg_setups: $(ETH2SPEC) # Delete all untracked files. clean: - @git clean -fdx \ No newline at end of file + @git clean -fdx diff --git a/configs/mainnet.yaml b/configs/mainnet.yaml index e54db49661..071f49288f 100644 --- a/configs/mainnet.yaml +++ b/configs/mainnet.yaml @@ -182,3 +182,8 @@ WHISK_PROPOSER_SELECTION_GAP: 2 # EIP7732 MAX_REQUEST_PAYLOADS: 128 + +# EIP7805 +ATTESTATION_DEADLINE: 4 +PROPOSER_INCLUSION_LIST_CUT_OFF: 11 +VIEW_FREEZE_DEADLINE: 9 \ No newline at end of file diff --git a/configs/minimal.yaml b/configs/minimal.yaml index a15314bb1f..275469ae8d 100644 --- a/configs/minimal.yaml +++ b/configs/minimal.yaml @@ -181,3 +181,8 @@ WHISK_PROPOSER_SELECTION_GAP: 1 # EIP7732 MAX_REQUEST_PAYLOADS: 128 + +# EIP7805 +ATTESTATION_DEADLINE: 2 +PROPOSER_INCLUSION_LIST_CUT_OFF: 5 +VIEW_FREEZE_DEADLINE: 3 diff --git a/pysetup/constants.py b/pysetup/constants.py index 0959e795c4..c4f7207afb 100644 --- a/pysetup/constants.py +++ b/pysetup/constants.py @@ -9,6 +9,7 @@ EIP6800 = 'eip6800' WHISK = 'whisk' EIP7732 = 'eip7732' +EIP7805 = 'eip7805' # The helper functions that are used when defining constants diff --git a/pysetup/helpers.py b/pysetup/helpers.py index b6141d4567..28eb099571 100644 --- a/pysetup/helpers.py +++ b/pysetup/helpers.py @@ -194,7 +194,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', 'Sequence', + 'Optional', 'Sequence', 'Tuple', ] diff --git a/pysetup/md_doc_paths.py b/pysetup/md_doc_paths.py index ff081ad5ab..df33688257 100644 --- a/pysetup/md_doc_paths.py +++ b/pysetup/md_doc_paths.py @@ -11,6 +11,7 @@ WHISK, EIP6800, EIP7732, + EIP7805, ) @@ -25,6 +26,7 @@ WHISK: CAPELLA, EIP6800: DENEB, EIP7732: ELECTRA, + EIP7805: ELECTRA, } ALL_FORKS = list(PREVIOUS_FORK_OF.keys()) diff --git a/pysetup/spec_builders/__init__.py b/pysetup/spec_builders/__init__.py index f6e05fc5b9..efbfeaf0f5 100644 --- a/pysetup/spec_builders/__init__.py +++ b/pysetup/spec_builders/__init__.py @@ -8,6 +8,7 @@ from .whisk import WhiskSpecBuilder from .eip6800 import EIP6800SpecBuilder from .eip7732 import EIP7732SpecBuilder +from .eip7805 import EIP7805SpecBuilder spec_builders = { @@ -15,5 +16,6 @@ for builder in ( Phase0SpecBuilder, AltairSpecBuilder, BellatrixSpecBuilder, CapellaSpecBuilder, DenebSpecBuilder, ElectraSpecBuilder, FuluSpecBuilder, WhiskSpecBuilder, EIP6800SpecBuilder, EIP7732SpecBuilder, + EIP7805SpecBuilder, ) } diff --git a/pysetup/spec_builders/eip7805.py b/pysetup/spec_builders/eip7805.py new file mode 100644 index 0000000000..35a7b770fa --- /dev/null +++ b/pysetup/spec_builders/eip7805.py @@ -0,0 +1,47 @@ +from .base import BaseSpecBuilder +from ..constants import EIP7805 + + +class EIP7805SpecBuilder(BaseSpecBuilder): + fork: str = EIP7805 + + + @classmethod + def execution_engine_cls(cls) -> str: + return """ +class NoopExecutionEngine(ExecutionEngine): + + def notify_new_payload(self: ExecutionEngine, + execution_payload: ExecutionPayload, + parent_beacon_block_root: Root, + execution_requests_list: Sequence[bytes], + inclusion_list_transactions: Sequence[Transaction]) -> bool: + return True + + 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 + + def get_payload(self: ExecutionEngine, payload_id: PayloadId) -> GetPayloadResponse: + # pylint: disable=unused-argument + raise NotImplementedError("no default block production") + + def is_valid_block_hash(self: ExecutionEngine, + execution_payload: ExecutionPayload, + parent_beacon_block_root: Root, + execution_requests_list: Sequence[bytes], + inclusion_list_transactions: Sequence[Transaction]) -> bool: + return True + + def is_valid_versioned_hashes(self: ExecutionEngine, new_payload_request: NewPayloadRequest) -> bool: + return True + + def verify_and_notify_new_payload(self: ExecutionEngine, + new_payload_request: NewPayloadRequest) -> bool: + return True + + +EXECUTION_ENGINE = NoopExecutionEngine()""" \ No newline at end of file diff --git a/specs/_features/eip7805/beacon-chain.md b/specs/_features/eip7805/beacon-chain.md new file mode 100644 index 0000000000..7245dd1bdc --- /dev/null +++ b/specs/_features/eip7805/beacon-chain.md @@ -0,0 +1,265 @@ +# EIP-7805 -- The Beacon Chain + +## Table of contents + + + + + +- [Introduction](#introduction) +- [Preset](#preset) + - [Domain types](#domain-types) + - [Inclusion List Committee](#inclusion-list-committee) + - [Execution](#execution) +- [Containers](#containers) + - [New containers](#new-containers) + - [`InclusionList`](#inclusionlist) + - [`SignedInclusionList`](#signedinclusionlist) + - [Predicates](#predicates) + - [New `is_valid_inclusion_list_signature`](#new-is_valid_inclusion_list_signature) + - [Beacon State accessors](#beacon-state-accessors) + - [New `get_inclusion_list_committee`](#new-get_inclusion_list_committee) +- [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) + - [Modified `is_valid_block_hash`](#modified-is_valid_block_hash) + - [Modified `notify_new_payload`](#modified-notify_new_payload) + - [Modified `verify_and_notify_new_payload`](#modified-verify_and_notify_new_payload) + - [Modified `process_execution_payload`](#modified-process_execution_payload) + + + + +## Introduction + +This is the beacon chain specification to add EIP-7805 / fork-choice enforced, committee-based inclusion list (FOCIL) mechanism to allow forced transaction inclusion. Refers to the following posts: +- [Fork-Choice enforced Inclusion Lists (FOCIL): A simple committee-based inclusion list proposal](https://ethresear.ch/t/fork-choice-enforced-inclusion-lists-focil-a-simple-committee-based-inclusion-list-proposal/19870/1) +- [FOCIL CL & EL workflow](https://ethresear.ch/t/focil-cl-el-workflow/20526) +*Note:* This specification is built upon [Electra](../../electra/beacon_chain.md) and is under active development. + +## Preset + +### Domain types + +| Name | Value | +| - | - | +| `DOMAIN_INCLUSION_LIST_COMMITTEE` | `DomainType('0x0C000000')` | + +### Inclusion List Committee + +| Name | Value | +| - | - | +| `INCLUSION_LIST_COMMITTEE_SIZE` | `uint64(2**4)` (=16) | + +### Execution + +| Name | Value | +| - | - | +| `MAX_TRANSACTIONS_PER_INCLUSION_LIST` | `uint64(1)` **TBD** | + +## Containers + +### New containers + +#### `InclusionList` + +```python +class InclusionList(Container): + slot: Slot + validator_index: ValidatorIndex + inclusion_list_committee_root: Root + transactions: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] +``` + +#### `SignedInclusionList` + +```python +class SignedInclusionList(Container): + message: InclusionList + signature: BLSSignature +``` + +### Predicates + +#### New `is_valid_inclusion_list_signature` + +```python +def is_valid_inclusion_list_signature( + state: BeaconState, + signed_inclusion_list: SignedInclusionList) -> bool: + """ + Check if ``signed_inclusion_list`` has a valid signature. + """ + message = signed_inclusion_list.message + index = message.validator_index + pubkey = state.validators[index].pubkey + domain = get_domain(state, DOMAIN_INCLUSION_LIST_COMMITTEE, compute_epoch_at_slot(message.slot)) + signing_root = compute_signing_root(message, domain) + return bls.Verify(pubkey, signing_root, signed_inclusion_list.signature) +``` + +### Beacon State accessors + +#### New `get_inclusion_list_committee` + +```python +def get_inclusion_list_committee(state: BeaconState, + slot: Slot) -> Vector[ValidatorIndex, INCLUSION_LIST_COMMITTEE_SIZE]: + epoch = compute_epoch_at_slot(slot) + seed = get_seed(state, epoch, DOMAIN_INCLUSION_LIST_COMMITTEE) + indices = get_active_validator_indices(state, epoch) + start = (slot % SLOTS_PER_EPOCH) * INCLUSION_LIST_COMMITTEE_SIZE + end = start + INCLUSION_LIST_COMMITTEE_SIZE + return [ + indices[compute_shuffled_index(uint64(i % len(indices)), uint64(len(indices)), seed)] + for i in range(start, end) + ] +``` + +## Beacon chain state transition function + +### Execution engine + +#### Request data + +##### Modified `NewPayloadRequest` + +```python +@dataclass +class NewPayloadRequest(object): + execution_payload: ExecutionPayload + versioned_hashes: Sequence[VersionedHash] + parent_beacon_block_root: Root + execution_requests: ExecutionRequests + inclusion_list_transactions: Sequence[Transaction] # [New in EIP-7805] +``` + +#### Engine APIs + +##### Modified `is_valid_block_hash` + +*Note*: The function `is_valid_block_hash` is modified to include the additional `inclusion_list_transactions`. + +```python +def is_valid_block_hash(self: ExecutionEngine, + execution_payload: ExecutionPayload, + parent_beacon_block_root: Root, + execution_requests_list: Sequence[bytes], + inclusion_list_transactions: Sequence[Transaction]) -> bool: + """ + Return ``True`` if and only if ``execution_payload.block_hash`` is computed correctly. + """ + ... +``` + +##### Modified `notify_new_payload` + +*Note*: The function `notify_new_payload` is modified to include the additional `inclusion_list_transactions`. + +```python +def notify_new_payload(self: ExecutionEngine, + execution_payload: ExecutionPayload, + parent_beacon_block_root: Root, + execution_requests_list: Sequence[bytes], + inclusion_list_transactions: Sequence[Transaction]) -> bool: + """ + Return ``True`` if and only if ``execution_payload`` and ``execution_requests_list`` + are valid with respect to ``self.execution_state``. + """ + # TODO: move this outside of notify_new_payload. + # If execution client returns block does not satisfy inclusion list transactions, cache the block + # store.unsatisfied_inclusion_list_blocks.add(execution_payload.block_root) + ... +``` + +##### Modified `verify_and_notify_new_payload` + +*Note*: The function `verify_and_notify_new_payload` is modified to pass the additional parameter +`inclusion_list_transactions` when calling `notify_new_payload` in EIP-7805. + +```python +def verify_and_notify_new_payload(self: ExecutionEngine, + new_payload_request: NewPayloadRequest) -> bool: + """ + Return ``True`` if and only if ``new_payload_request`` is valid with respect to ``self.execution_state``. + """ + execution_payload = new_payload_request.execution_payload + parent_beacon_block_root = new_payload_request.parent_beacon_block_root + execution_requests_list = get_execution_requests_list(new_payload_request.execution_requests) + inclusion_list_transactions = new_payload_request.inclusion_list_transactions # [New in EIP-7805] + + if b'' in execution_payload.transactions: + return False + + if not self.is_valid_block_hash( + execution_payload, + parent_beacon_block_root, + execution_requests_list): + return False + + if not self.is_valid_versioned_hashes(new_payload_request): + return False + + # [Modified in EIP-7805] + if not self.notify_new_payload( + execution_payload, + parent_beacon_block_root, + execution_requests_list, + inclusion_list_transactions): + return False + + return True +``` + +##### Modified `process_execution_payload` + +```python +def process_execution_payload(state: BeaconState, body: BeaconBlockBody, execution_engine: ExecutionEngine) -> None: + payload = body.execution_payload + + # Verify consistency of the parent hash with respect to the previous execution payload header + 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 commitments are under limit + assert len(body.blob_kzg_commitments) <= MAX_BLOBS_PER_BLOCK_ELECTRA + # Verify the execution payload is valid + versioned_hashes = [kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments] + # Verify inclusion list transactions + inclusion_list_transactions: Sequence[Transaction] = [] # TODO: where do we get this? + assert len(inclusion_list_transactions) <= MAX_TRANSACTIONS_PER_INCLUSION_LIST + # Verify the payload with the execution engine + assert execution_engine.verify_and_notify_new_payload( + NewPayloadRequest( + execution_payload=payload, + versioned_hashes=versioned_hashes, + parent_beacon_block_root=state.latest_block_header.parent_root, + execution_requests=body.execution_requests, + inclusion_list_transactions=inclusion_list_transactions, + ) + ) + # 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), + blob_gas_used=payload.blob_gas_used, + excess_blob_gas=payload.excess_blob_gas, + ) +``` diff --git a/specs/_features/eip7805/fork-choice.md b/specs/_features/eip7805/fork-choice.md new file mode 100644 index 0000000000..a8cf0b7fc8 --- /dev/null +++ b/specs/_features/eip7805/fork-choice.md @@ -0,0 +1,206 @@ +# EIP-7805 -- Fork Choice + +## Table of contents + + + + +- [Introduction](#introduction) +- [Configuration](#configuration) + - [Time parameters](#time-parameters) +- [Fork choice](#fork-choice) + - [Helpers](#helpers) + - [Modified `Store`](#modified-store) + - [New `validate_inclusion_lists`](#new-validate_inclusion_lists) + - [New `get_attester_head`](#new-get_attester_head) + - [New `on_inclusion_list`](#new-on_inclusion_list) + + + + +## Introduction + +This is the modification of the fork choice accompanying the EIP-7805 upgrade. + +## Configuration + +### Time parameters + +| Name | Value | Unit | Duration | +| - | - | :-: | :-: | +| `VIEW_FREEZE_DEADLINE` | `SECONDS_PER_SLOT * 2 // 3 + 1` | seconds | 9 seconds | + +## Fork choice + +### Helpers + +#### Modified `Store` + +**Note:** `Store` is modified to track the seen inclusion lists and inclusion list equivocators. + +```python +@dataclass +class Store(object): + time: uint64 + genesis_time: uint64 + justified_checkpoint: Checkpoint + finalized_checkpoint: Checkpoint + unrealized_justified_checkpoint: Checkpoint + unrealized_finalized_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) + block_timeliness: Dict[Root, boolean] = field(default_factory=dict) + checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict) + latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict) + unrealized_justifications: Dict[Root, Checkpoint] = field(default_factory=dict) + # [New in EIP-7805] + inclusion_lists: Dict[Tuple[Slot, Root], List[InclusionList]] = field(default_factory=dict) + inclusion_list_equivocators: Dict[Tuple[Slot, Root], Set[ValidatorIndex]] = field(default_factory=dict) + unsatisfied_inclusion_list_blocks: Set[Root] = field(default_factory=Set) +``` + +#### New `validate_inclusion_lists` + +```python +def validate_inclusion_lists(store: Store, + inclusion_list_transactions: Sequence[Transaction], + execution_payload: ExecutionPayload) -> None: + """ + The ``execution_payload`` satisfies ``inclusion_list_transactions`` validity conditions either + when all transactions are present in payload or when any missing transactions are found to be + invalid when appended to the end of the payload unless the block is full. + """ + # pylint: disable=unused-argument + + # Verify inclusion list is a valid length + assert len(inclusion_list_transactions) <= MAX_TRANSACTIONS_PER_INCLUSION_LIST * INCLUSION_LIST_COMMITTEE_SIZE + + # Verify inclusion list transactions are present in the execution payload + contains_all_txs = all(tx in execution_payload.transactions for tx in inclusion_list_transactions) + if contains_all_txs: + return + + # TODO: check remaining validity conditions +``` + +#### New `get_attester_head` + +```python +def get_attester_head(store: Store, head_root: Root) -> Root: + head_block = store.blocks[head_root] + + if head_root in store.unsatisfied_inclusion_list_blocks: + return head_block.parent_root + return head_root + +``` + +##### Modified `get_proposer_head` + +The implementation of `get_proposer_head` is modified to also account for `store.unsatisfied_inclusion_list_blocks`. + +```python +def get_proposer_head(store: Store, head_root: Root, slot: Slot) -> Root: + head_block = store.blocks[head_root] + parent_root = head_block.parent_root + parent_block = store.blocks[parent_root] + + # Only re-org the head block if it arrived later than the attestation deadline. + head_late = is_head_late(store, head_root) + + # Do not re-org on an epoch boundary where the proposer shuffling could change. + shuffling_stable = is_shuffling_stable(slot) + + # Ensure that the FFG information of the new head will be competitive with the current head. + ffg_competitive = is_ffg_competitive(store, head_root, parent_root) + + # Do not re-org if the chain is not finalizing with acceptable frequency. + finalization_ok = is_finalization_ok(store, slot) + + # Only re-org if we are proposing on-time. + proposing_on_time = is_proposing_on_time(store) + + # Only re-org a single slot at most. + parent_slot_ok = parent_block.slot + 1 == head_block.slot + current_time_ok = head_block.slot + 1 == slot + single_slot_reorg = parent_slot_ok and current_time_ok + + # Check that the head has few enough votes to be overpowered by our proposer boost. + assert store.proposer_boost_root != head_root # ensure boost has worn off + head_weak = is_head_weak(store, head_root) + + # Check that the missing votes are assigned to the parent and not being hoarded. + parent_strong = is_parent_strong(store, parent_root) + + + reorg_prerequisites = all([shuffling_stable, ffg_competitive, finalization_ok, + proposing_on_time, single_slot_reorg, head_weak, parent_strong]) + + # Check that the head block is in the unsatisfied inclusion list blocks + inclusion_list_not_satisfied = head_root in store.unsatisfied_inclusion_list_blocks # [New in EIP-7805] + + if reorg_prerequisites and (head_late or inclusion_list_not_satisfied): + return parent_root + else: + return head_root +``` + +#### New `on_inclusion_list` + +`on_inclusion_list` is called to import `signed_inclusion_list` to the fork choice store. + +```python +def on_inclusion_list( + store: Store, + state: BeaconState, + signed_inclusion_list: SignedInclusionList, + inclusion_list_committee: Vector[ValidatorIndex, INCLUSION_LIST_COMMITTEE_SIZE]) -> None: + """ + Verify the inclusion list and import it into the fork choice store. If there exists more than + one inclusion list in the store with the same slot and validator index, add the equivocator to + the ``inclusion_list_equivocators`` cache. Otherwise, add the inclusion list to the + ``inclusion_lists` cache. + """ + message = signed_inclusion_list.message + + # Verify inclusion list slot is either from the current or previous slot + assert get_current_slot(store) in [message.slot, message.slot + 1] + + time_into_slot = (store.time - store.genesis_time) % SECONDS_PER_SLOT + is_before_attesting_interval = time_into_slot < SECONDS_PER_SLOT // INTERVALS_PER_SLOT + + # If the inclusion list is from the previous slot, ignore it if already past the attestation deadline + if get_current_slot(store) == message.slot + 1: + assert is_before_attesting_interval + + # Sanity check that the given `inclusion_list_committee` matches the root in the inclusion list + root = message.inclusion_list_committee_root + assert hash_tree_root(inclusion_list_committee) == root + + # Verify inclusion list validator is part of the committee + validator_index = message.validator_index + assert validator_index in inclusion_list_committee + + # Verify inclusion list signature + assert is_valid_inclusion_list_signature(state, signed_inclusion_list) + + is_before_freeze_deadline = get_current_slot(store) == message.slot and time_into_slot < VIEW_FREEZE_DEADLINE + + # Do not process inclusion lists from known equivocators + if validator_index not in store.inclusion_list_equivocators[(message.slot, root)]: + if validator_index in [il.validator_index for il in store.inclusion_lists[(message.slot, root)]]: + validator_inclusion_list = [ + il for il in store.inclusion_lists[(message.slot, root)] + if il.validator_index == validator_index + ][0] + if validator_inclusion_list != message: + # We have equivocation evidence for `validator_index`, record it as equivocator + store.inclusion_list_equivocators[(message.slot, root)].add(validator_index) + # This inclusion list is not an equivocation. Store it if prior to the view freeze deadline + elif is_before_freeze_deadline: + store.inclusion_lists[(message.slot, root)].append(message) +``` + + diff --git a/specs/_features/eip7805/p2p-interface.md b/specs/_features/eip7805/p2p-interface.md new file mode 100644 index 0000000000..30a858f439 --- /dev/null +++ b/specs/_features/eip7805/p2p-interface.md @@ -0,0 +1,90 @@ +# EIP-7805 -- Networking + +This document contains the consensus-layer networking specification for EIP-7805. + + + + +- [Modifications in EIP-7805](#modifications-in-eip-7805) + - [Configuration](#configuration) + - [The gossip domain: gossipsub](#the-gossip-domain-gossipsub) + - [Topics and messages](#topics-and-messages) + - [Global topics](#global-topics) + - [`inclusion_list`](#inclusion_list) + - [The Req/Resp domain](#the-reqresp-domain) + - [Messages](#messages) + - [InclusionListByCommitteeIndices v1](#inclusionlistbycommitteeindices-v1) + + + +## Modifications in EIP-7805 + +### Configuration + +| Name | Value | Unit | Duration | +| - | - | :-: | :-: | +| `ATTESTATION_DEADLINE` | `SECONDS_PER_SLOT // 3` | seconds | 4 seconds | + +| Name | Value | Description | +| - | - | - | +| `MAX_REQUEST_INCLUSION_LIST` | `2**4` (= 16) | Maximum number of inclusion list in a single request | +| `MAX_BYTES_PER_INCLUSION_LIST` | `2**13` (= 8192) | Maximum size of inclusion list in bytes | + +### The gossip domain: gossipsub + +#### Topics and messages + +The new topics along with the type of the `data` field of a gossipsub message are given in this table: + +| Name | Message Type | +| - | - | +| `inclusion_list` | `SignedInclusionList` | + +##### Global topics + +EIP-7805 introduces a new global topic for inclusion lists. + +###### `inclusion_list` + +This topic is used to propagate signed inclusion list as `SignedInclusionList`. +The following validations MUST pass before forwarding the `inclusion_list` on the network, assuming the alias `message = signed_inclusion_list.message`: + +- _[REJECT]_ The size of `message` is within upperbound `MAX_BYTES_PER_INCLUSION_LIST`. +- _[REJECT]_ The slot `message.slot` is equal to the previous or current slot. +- _[IGNORE]_ The slot `message.slot` is equal to the current slot, or it is equal to the previous slot and the current time is less than `ATTESTATION_DEADLINE` seconds into the slot. +- _[IGNORE]_ The `inclusion_list_committee` for slot `message.slot` on the current branch corresponds to `message.inclusion_list_committee_root`, as determined by `hash_tree_root(inclusion_list_committee) == message.inclusion_list_committee_root`. +- _[REJECT]_ The validator index `message.validator_index` is within the `inclusion_list_committee` corresponding to `message.inclusion_list_committee_root`. +- _[REJECT]_ The transactions `message.transactions` length is within upperbound `MAX_TRANSACTIONS_PER_INCLUSION_LIST`. +- _[IGNORE]_ The `message` is either the first or second valid message received from the validator with index `message.validator_index`. +- _[REJECT]_ The signature of `inclusion_list.signature` is valid with respect to the validator index. + +### The Req/Resp domain + +#### Messages + +##### InclusionListByCommitteeIndices v1 + +**Protocol ID:** `/eth2/beacon_chain/req/inclusion_list_by_committee_indices/1/` + +The `` field is calculated as `context = compute_fork_digest(fork_version, genesis_validators_root)`: + +[1]: # (eth2spec: skip) + +| `fork_version` | Chunk SSZ type | +|------------------------|--------------------------------| +| `EIP7805_FORK_VERSION` | `EIP-7805.SignedInclusionList` | + +Request Content: +``` +( + slot: Slot + committee_indices: Bitvector[INCLUSION_LIST_COMMITTEE_SIZE] +) +``` + +Response Content: +``` +( + List[SignedInclusionList, MAX_REQUEST_INCLUSION_LIST] +) +``` diff --git a/specs/_features/eip7805/validator.md b/specs/_features/eip7805/validator.md new file mode 100644 index 0000000000..344c5ec33a --- /dev/null +++ b/specs/_features/eip7805/validator.md @@ -0,0 +1,150 @@ +# EIP-7805 -- Honest Validator + +## Table of contents + + + + + +- [Introduction](#introduction) +- [Prerequisites](#prerequisites) +- [Configuration](#configuration) + - [Time parameters](#time-parameters) +- [Protocol](#protocol) + - [`ExecutionEngine`](#executionengine) +- [New inclusion list committee assignment](#new-inclusion-list-committee-assignment) + - [Lookahead](#lookahead) +- [New proposer duty](#new-proposer-duty) + - [Block proposal](#block-proposal) + - [Update execution client with inclusion lists](#update-execution-client-with-inclusion-lists) +- [New inclusion list committee duty](#new-inclusion-list-committee-duty) + - [Constructing a signed inclusion list](#constructing-a-signed-inclusion-list) +- [Modified attester duty](#modified-attester-duty) + - [Modified LMD GHOST vote](#modified-lmd-ghost-vote) +- [Modified sync committee duty](#modified-sync-committee-duty) + - [Modified beacon block root](#modified-beacon-block-root) + + + + +## Introduction + +This document represents the changes to be made in the code of an "honest validator" to implement EIP-7805. + +## Prerequisites + +This document is an extension of the [Electra -- Honest Validator](../../electra/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 [EIP-7805](./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. + +## Configuration + +### Time parameters + +| Name | Value | Unit | Duration | +| - | - | :-: | :-: | +| `PROPOSER_INCLUSION_LIST_CUT_OFF` | `SECONDS_PER_SLOT - 1` | seconds | 11 seconds | + +## Protocol + +### `ExecutionEngine` + +*Note*: `engine_getInclusionListV1` and `engine_updateBlockWithInclusionListV1` functions are added to the `ExecutionEngine` protocol for use as a validator. + +The body of these function is implementation dependent. The Engine API may be used to implement it with an external execution engine. + +## New inclusion list committee assignment + +A validator may be a member of the new Inclusion List Committee (ILC) for a given slot. To check for ILC assignments the validator uses the helper `get_inclusion_committee_assignment(state, epoch, validator_index)` where `epoch <= next_epoch`. + +Inclusion list committee selection is only stable within the context of the current and next epoch. + +```python +def get_inclusion_committee_assignment( + state: BeaconState, + epoch: Epoch, + validator_index: ValidatorIndex) -> Optional[Slot]: + """ + Returns the slot during the requested epoch in which the validator with index ``validator_index`` + is a member of the ILC. Returns None if no assignment is found. + """ + next_epoch = Epoch(get_current_epoch(state) + 1) + assert epoch <= next_epoch + + start_slot = compute_start_slot_at_epoch(epoch) + for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH): + if validator_index in get_inclusion_list_committee(state, Slot(slot)): + return Slot(slot) + return None +``` + +### Lookahead + +`get_inclusion_committee_assignment` should be called at the start of each epoch to get the assignment for the next epoch (`current_epoch + 1`). A validator should plan for future assignments by noting their assigned ILC slot. + +## New proposer duty + +### Block proposal + +Proposers are still expected to propose `SignedBeaconBlock` at the beginning of any slot during which `is_proposer(state, validator_index)` returns true. The mechanism to prepare this beacon block and related sidecars differs from previous forks as follows: + +#### Update execution client with inclusion lists + +The proposer should call `engine_updateInclusionListV1` at `PROPOSER_INCLUSION_LIST_CUT_OFF` into the slot with the list of the inclusion lists that gathered up to `PROPOSER_INCLUSION_LIST_CUT_OFF`. + +## New inclusion list committee duty + +Some validators are selected to submit signed inclusion list. Validators should call `get_inclusion_committee_assignment` at the beginning of an epoch to be prepared to submit their inclusion list during the next epoch. + +A validator should create and broadcast the `signed_inclusion_list` to the global `inclusion_list` subnet by `PROPOSER_INCLUSION_LIST_CUT_OFF` seconds into the slot, unless a block for the current slot has been processed and is the head of the chain and broadcast to the network. + +#### Constructing a signed inclusion list + +The validator creates the `signed_inclusion_list` as follows: +- First, the validator creates the `inclusion_list`. +- Set `inclusion_list.slot` to the assigned slot returned by `get_inclusion_committee_assignment`. +- Set `inclusion_list.validator_index` to the validator's index. +- Set `inclusion_list.inclusion_list_committee_root` to the hash tree root of the committee that the validator is a member of. +- Set `inclusion_list.transactions` using the response from `engine_getInclusionListV1` from the execution layer client. +- Sign the `inclusion_list` using the helper `get_inclusion_list_signature` and obtain the `signature`. +- Set `signed_inclusion_list.message` to `inclusion_list`. +- Set `signed_inclusion_list.signature` to `signature`. + +```python +def get_inclusion_list_signature( + state: BeaconState, inclusion_list: InclusionList, privkey: int) -> BLSSignature: + domain = get_domain(state, DOMAIN_INCLUSION_LIST_COMMITTEE, compute_epoch_at_slot(inclusion_list.slot)) + signing_root = compute_signing_root(inclusion_list, domain) + return bls.Sign(privkey, signing_root) +``` + +## Modified attester duty + +#### Modified LMD GHOST vote + +Set `attestation_data.beacon_block_root = get_attester_head(store, head_root)`. + +## Modified sync committee duty + +#### Modified beacon block root + +```python +def get_sync_committee_message(state: BeaconState, + block_root: Root, + validator_index: ValidatorIndex, + privkey: int, + store: Store) -> SyncCommitteeMessage: + epoch = get_current_epoch(state) + domain = get_domain(state, DOMAIN_SYNC_COMMITTEE, epoch) + signing_root = compute_signing_root(block_root, domain) + signature = bls.Sign(privkey, signing_root) + + return SyncCommitteeMessage( + slot=state.slot, + beacon_block_root=get_attester_head(store, block_root), + validator_index=validator_index, + signature=signature, + ) +``` 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 bf16a1b51b..fee57e3b95 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 @@ -11,10 +11,13 @@ spec_state_test, with_altair_and_later, with_presets, + with_all_phases_from_except, ) from eth2spec.test.helpers.constants import ( MAINNET, MINIMAL, + ALTAIR, + EIP7805, ) rng = random.Random(1337) @@ -139,7 +142,7 @@ def test_process_sync_committee_contributions(spec, state): spec.process_block(state, block) -@with_altair_and_later +@with_all_phases_from_except(ALTAIR, [EIP7805]) @spec_state_test @always_bls def test_get_sync_committee_message(spec, state): diff --git a/tests/core/pyspec/eth2spec/test/helpers/constants.py b/tests/core/pyspec/eth2spec/test/helpers/constants.py index 627243be87..0c0b1c91c6 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/constants.py +++ b/tests/core/pyspec/eth2spec/test/helpers/constants.py @@ -21,6 +21,7 @@ FULU = SpecForkName('fulu') WHISK = SpecForkName('whisk') EIP7732 = SpecForkName('eip7732') +EIP7805 = SpecForkName('eip7805') # # SpecFork settings @@ -59,6 +60,7 @@ WHISK: CAPELLA, FULU: ELECTRA, EIP7732: ELECTRA, + EIP7805: ELECTRA, } # For fork transition tests