diff --git a/pysetup/spec_builders/eip7732.py b/pysetup/spec_builders/eip7732.py index d3d7f6d384..a90027d202 100644 --- a/pysetup/spec_builders/eip7732.py +++ b/pysetup/spec_builders/eip7732.py @@ -14,12 +14,63 @@ def imports(cls, preset_name: str): @classmethod def sundry_functions(cls) -> str: return """ +def cached_or_new_inclusion_list_store() -> InclusionListStore: + # pylint: disable=unused-argument + return InclusionListStore() + def concat_generalized_indices(*indices: GeneralizedIndex) -> GeneralizedIndex: o = GeneralizedIndex(1) for i in indices: o = GeneralizedIndex(o * bit_floor(i) + (i - bit_floor(i))) return o""" + @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]) -> 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]) -> 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 + + def get_inclusion_list(self: ExecutionEngine) -> GetInclusionListResponse: + # pylint: disable=unused-argument + raise NotImplementedError("no default inclusion list production") + + def is_inclusion_list_satisfied(self: ExecutionEngine, + execution_payload: ExecutionPayload, + inclusion_list_transactions: Sequence[Transaction]) -> bool: + return True + + +EXECUTION_ENGINE = NoopExecutionEngine()""" + @classmethod def deprecate_constants(cls) -> set[str]: return set( diff --git a/specs/_features/eip7732/beacon-chain.md b/specs/_features/eip7732/beacon-chain.md index f92509f30b..f87c45dfe7 100644 --- a/specs/_features/eip7732/beacon-chain.md +++ b/specs/_features/eip7732/beacon-chain.md @@ -24,6 +24,8 @@ - [`SignedExecutionPayloadHeader`](#signedexecutionpayloadheader) - [`ExecutionPayloadEnvelope`](#executionpayloadenvelope) - [`SignedExecutionPayloadEnvelope`](#signedexecutionpayloadenvelope) + - [`InclusionList`](#inclusionlist) + - [`SignedInclusionList`](#signedinclusionlist) - [Modified containers](#modified-containers) - [`BeaconBlockBody`](#beaconblockbody) - [`ExecutionPayloadHeader`](#executionpayloadheader) @@ -40,11 +42,13 @@ - [New `is_builder_withdrawal_credential`](#new-is_builder_withdrawal_credential) - [New `is_valid_indexed_payload_attestation`](#new-is_valid_indexed_payload_attestation) - [New `is_parent_block_full`](#new-is_parent_block_full) + - [New `is_valid_inclusion_list_signature`](#new-is_valid_inclusion_list_signature) - [Beacon State accessors](#beacon-state-accessors) - [New `get_attestation_participation_flag_indices`](#new-get_attestation_participation_flag_indices) - [New `get_ptc`](#new-get_ptc) - [New `get_payload_attesting_indices`](#new-get_payload_attesting_indices) - [New `get_indexed_payload_attestation`](#new-get_indexed_payload_attestation) + - [New `get_inclusion_list_committee`](#new-get_inclusion_list_committee) - [Beacon chain state transition function](#beacon-chain-state-transition-function) - [Modified `process_slot`](#modified-process_slot) - [Epoch processing](#epoch-processing) @@ -106,10 +110,11 @@ At any given slot, the status of the blockchain's head may be either ### Domain types -| Name | Value | -| ----------------------- | -------------------------- | -| `DOMAIN_BEACON_BUILDER` | `DomainType('0x1B000000')` | -| `DOMAIN_PTC_ATTESTER` | `DomainType('0x0C000000')` | +| Name | Value | +| --------------------------------- | -------------------------- | +| `DOMAIN_BEACON_BUILDER` | `DomainType('0x1B000000')` | +| `DOMAIN_PTC_ATTESTER` | `DomainType('0x0C000000')` | +| `DOMAIN_INCLUSION_LIST_COMMITTEE` | `DomainType('0x0C000000')` | ### Misc @@ -122,9 +127,10 @@ At any given slot, the status of the blockchain's head may be either ### Misc -| Name | Value | -| ---------- | --------------------- | -| `PTC_SIZE` | `uint64(2**9)` (=512) | +| Name | Value | +| ------------------------------- | --------------------- | +| `PTC_SIZE` | `uint64(2**9)` (=512) | +| `INCLUSION_LIST_COMMITTEE_SIZE` | `uint64(2**4)` (=16) | ### Max operations per block @@ -232,6 +238,24 @@ class SignedExecutionPayloadEnvelope(Container): signature: BLSSignature ``` +#### `InclusionList` + +```python +class InclusionList(Container): + slot: Slot + validator_index: ValidatorIndex + inclusion_list_committee_root: Root + transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD] +``` + +#### `SignedInclusionList` + +```python +class SignedInclusionList(Container): + message: InclusionList + signature: BLSSignature +``` + ### Modified containers #### `BeaconBlockBody` @@ -264,6 +288,7 @@ class BeaconBlockBody(Container): signed_execution_payload_header: SignedExecutionPayloadHeader # [New in EIP7732] payload_attestations: List[PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS] + inclusion_list_bits: Bitvector[INCLUSION_LIST_COMMITTEE_SIZE] ``` #### `ExecutionPayloadHeader` @@ -457,6 +482,23 @@ def is_parent_block_full(state: BeaconState) -> bool: return state.latest_execution_payload_header.block_hash == state.latest_block_hash ``` +#### 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_attestation_participation_flag_indices` @@ -554,6 +596,25 @@ def get_indexed_payload_attestation( ) ``` +#### 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 Vector[ValidatorIndex, INCLUSION_LIST_COMMITTEE_SIZE]( + [ + indices[compute_shuffled_index(uint64(i % len(indices)), uint64(len(indices)), seed)] + for i in range(start, end) + ] + ) +``` + ## Beacon chain state transition function *Note*: state transition is fundamentally modified in EIP-7732. The full state diff --git a/specs/_features/eip7732/fork-choice.md b/specs/_features/eip7732/fork-choice.md index 8d0b55fa9f..8141fcbf6c 100644 --- a/specs/_features/eip7732/fork-choice.md +++ b/specs/_features/eip7732/fork-choice.md @@ -6,11 +6,19 @@ - [Introduction](#introduction) - [Custom types](#custom-types) +- [Configuration](#configuration) + - [Time parameters](#time-parameters) - [Constants](#constants) - [Containers](#containers) - [New `ForkChoiceNode`](#new-forkchoicenode) +- [Protocols](#protocols) + - [`ExecutionEngine`](#executionengine) + - [New `is_inclusion_list_satisfied`](#new-is_inclusion_list_satisfied) + - [Modified `notify_forkchoice_updated`](#modified-notify_forkchoice_updated) +- [Helpers](#helpers) - [Modified `LatestMessage`](#modified-latestmessage) - [Modified `update_latest_messages`](#modified-update_latest_messages) + - [Modified `PayloadAttributes`](#modified-payloadattributes) - [Modified `Store`](#modified-store) - [Modified `get_forkchoice_store`](#modified-get_forkchoice_store) - [New `notify_ptc_messages`](#new-notify_ptc_messages) @@ -25,11 +33,16 @@ - [Modified `get_weight`](#modified-get_weight) - [New `get_node_children`](#new-get_node_children) - [Modified `get_head`](#modified-get_head) + - [New `is_inclusion_list_satisfied_block`](#new-is_inclusion_list_satisfied_block) + - [New `is_inclusion_list_satisfied_payload`](#new-is_inclusion_list_satisfied_payload) + - [New `get_attester_head`](#new-get_attester_head) + - [Modified `get_proposer_head`](#modified-get_proposer_head) - [Updated fork-choice handlers](#updated-fork-choice-handlers) - [Modified `on_block`](#modified-on_block) - [New fork-choice handlers](#new-fork-choice-handlers) - [New `on_execution_payload`](#new-on_execution_payload) - [New `on_payload_attestation_message`](#new-on_payload_attestation_message) + - [New `on_inclusion_list`](#new-on_inclusion_list) - [Modified `validate_on_attestation`](#modified-validate_on_attestation) - [Modified `validate_merge_block`](#modified-validate_merge_block) @@ -45,6 +58,14 @@ This is the modification of the fork-choice accompanying the EIP-7732 upgrade. | --------------- | -------------- | ----------------------------------------------- | | `PayloadStatus` | `uint8` | Possible status of a payload in the fork-choice | +## Configuration + +### Time parameters + +| Name | Value | Unit | Duration | +| ---------------------- | ------------------------------- | :-----: | :-------: | +| `VIEW_FREEZE_DEADLINE` | `SECONDS_PER_SLOT * 2 // 3 + 1` | seconds | 9 seconds | + ## Constants | Name | Value | @@ -65,6 +86,55 @@ class ForkChoiceNode(Container): payload_status: PayloadStatus # One of PAYLOAD_STATUS_* values ``` +## Protocols + +### `ExecutionEngine` + +*Note*: The `is_inclusion_list_satisfied` function is added to the +`ExecutionEngine` protocol to instantiate the inclusion list constraints +validation. + +The body of this function is implementation dependent. The Engine API may be +used to implement it with an external execution engine. + +#### New `is_inclusion_list_satisfied` + +```python +def is_inclusion_list_satisfied( + self: ExecutionEngine, + execution_payload: ExecutionPayload, + inclusion_list_transactions: Sequence[Transaction], +) -> bool: + """ + Return ``True`` if and only if ``execution_payload`` satisfies the inclusion + list constraints with respect to ``inclusion_list_transactions``. + """ + ... +``` + +#### Modified `notify_forkchoice_updated` + +The only change made is to the `PayloadAttributes` container through the +addition of `inclusion_list_transactions`. Otherwise, +`notify_forkchoice_updated` inherits all prior functionality. + +*Note*: If the `inclusion_list_transactions` field of `payload_attributes` is +not empty, the payload build process MUST produce an execution payload that +satisfies the inclusion list constraints with respect to +`inclusion_list_transactions`. + +```python +def notify_forkchoice_updated( + self: ExecutionEngine, + head_block_hash: Hash32, + safe_block_hash: Hash32, + finalized_block_hash: Hash32, + payload_attributes: Optional[PayloadAttributes], +) -> Optional[PayloadId]: ... +``` + +## Helpers + ### Modified `LatestMessage` *Note*: The class is modified to keep track of the slot instead of the epoch. @@ -102,6 +172,22 @@ def update_latest_messages( ) ``` +### Modified `PayloadAttributes` + +`PayloadAttributes` is extended with the `inclusion_list_transactions` field. + +```python +@dataclass +class PayloadAttributes(object): + timestamp: uint64 + prev_randao: Bytes32 + suggested_fee_recipient: ExecutionAddress + withdrawals: Sequence[Withdrawal] + parent_beacon_block_root: Root + # [New in EIP7805] + inclusion_list_transactions: Sequence[Transaction] +``` + ### Modified `Store` *Note*: `Store` is modified to track the intermediate states of "empty" @@ -129,6 +215,8 @@ class Store(object): execution_payload_states: Dict[Root, BeaconState] = field(default_factory=dict) # [New in EIP7732] ptc_vote: Dict[Root, Vector[boolean, PTC_SIZE]] = field(default_factory=dict) + # [New in EIP7805] + unsatisfied_inclusion_list_payloads: Set[Hash32] = field(default_factory=Set) ``` ### Modified `get_forkchoice_store` @@ -157,6 +245,8 @@ def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) - # [New in EIP7732] execution_payload_states={anchor_root: copy(anchor_state)}, ptc_vote={anchor_root: Vector[boolean, PTC_SIZE]()}, + # [New in EIP7805] + unsatisfied_inclusion_list_payloads=set(), ) ``` @@ -440,6 +530,121 @@ def get_head(store: Store) -> ForkChoiceNode: ) ``` +### New `is_inclusion_list_satisfied_block` + +```python +def is_inclusion_list_satisfied_block(store: Store, block_root: Root) -> bool: + inclusion_list_store = get_inclusion_list_store() + + block = store.blocks[block_root] + state = store.block_states[block_root] + payload_header = block.body.signed_execution_payload_header.message + + inclusion_list_bits_inclusive = is_inclusion_list_bits_inclusive( + inclusion_list_store, state, block.slot, block.body.inclusion_list_bits + ) + inclusion_list_transactions_satisfied = ( + not is_parent_node_full(store, block) + or payload_header.parent_block_hash not in store.unsatisfied_inclusion_list_payloads + ) + + return inclusion_list_bits_inclusive and inclusion_list_transactions_satisfied +``` + +### New `is_inclusion_list_satisfied_payload` + +```python +def is_inclusion_list_satisfied_payload( + store: Store, + block_root: Root, + payload: ExecutionPayload, + execution_engine: ExecutionEngine, +) -> bool: + inclusion_list_store = get_inclusion_list_store() + + block = store.blocks[block_root] + state = store.block_states[block_root] + + inclusion_list_transactions = get_inclusion_list_transactions( + inclusion_list_store, state, Slot(block.slot - 1) + ) + + return execution_engine.is_inclusion_list_satisfied(payload, inclusion_list_transactions) +``` + +### New `get_attester_head` + +```python +def get_attester_head(store: Store, head_root: Root) -> Root: + is_inclusion_list_satisfied = is_inclusion_list_satisfied_block(store, head_root) + + if not is_inclusion_list_satisfied: + head_block = store.blocks[head_root] + 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_payloads`. + +```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, + ] + ) + + # [New in EIP7805] + # Check that the head block satisfies the inclusion list constraints + inclusion_list_satisfied = is_inclusion_list_satisfied_block(store, head_root) + + if reorg_prerequisites and (head_late or not inclusion_list_satisfied): # [Modified in EIP-7805] + return parent_root + else: + return head_root +``` + ## Updated fork-choice handlers ### Modified `on_block` @@ -506,7 +711,10 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: # Add proposer score boost if the block is timely and not conflicting with an existing block is_first_block = store.proposer_boost_root == Root() - if is_timely and is_first_block: + is_inclusion_list_satisfied = is_inclusion_list_satisfied_block( + store, block_root + ) # [New in EIP-7805] + if is_timely and is_first_block and is_inclusion_list_satisfied: # [Modified in EIP-7805] store.proposer_boost_root = hash_tree_root(block) # Update checkpoints in store if necessary @@ -542,6 +750,15 @@ def on_execution_payload(store: Store, signed_envelope: SignedExecutionPayloadEn # Process the execution payload process_execution_payload(state, signed_envelope, EXECUTION_ENGINE) + # [New in EIP7805] + # Check if payload satisfies the inclusion list constraints + # If not, add this payload to the store as inclusion list unsatisfied + is_inclusion_list_satisfied = is_inclusion_list_satisfied_payload( + store, envelope.beacon_block_root, envelope.payload, EXECUTION_ENGINE + ) + if not is_inclusion_list_satisfied: + store.unsatisfied_inclusion_list_payloads.add(envelope.payload.block_hash) + # Add new state for this payload to the store store.execution_payload_states[envelope.beacon_block_root] = state ``` @@ -585,6 +802,25 @@ def on_payload_attestation_message( ptc_vote[ptc_index] = data.payload_present ``` +### New `on_inclusion_list` + +```python +def on_inclusion_list(store: Store, signed_inclusion_list: SignedInclusionList) -> None: + """ + Run ``on_inclusion_list`` upon receiving a new inclusion list. + """ + inclusion_list = signed_inclusion_list.message + + inclusion_list_store = get_inclusion_list_store() + + time_into_slot = (store.time - store.genesis_time) % SECONDS_PER_SLOT + is_before_view_freeze_deadline = ( + get_current_slot(store) == inclusion_list.slot and time_into_slot < VIEW_FREEZE_DEADLINE + ) + + process_inclusion_list(inclusion_list_store, inclusion_list, is_before_view_freeze_deadline) +``` + ### Modified `validate_on_attestation` ```python diff --git a/specs/_features/eip7732/inclusion-list.md b/specs/_features/eip7732/inclusion-list.md new file mode 100644 index 0000000000..e7a135c936 --- /dev/null +++ b/specs/_features/eip7732/inclusion-list.md @@ -0,0 +1,147 @@ +# EIP-7805 -- Inclusion List + + + +- [Introduction](#introduction) +- [Containers](#containers) + - [New containers](#new-containers) + - [`InclusionListStore`](#inclusionliststore) +- [Helpers](#helpers) + - [New `get_inclusion_list_store`](#new-get_inclusion_list_store) + - [New `process_inclusion_list`](#new-process_inclusion_list) + - [New `get_inclusion_list_transactions`](#new-get_inclusion_list_transactions) + - [New `get_inclusion_list_bits`](#new-get_inclusion_list_bits) + - [New `is_inclusion_list_bits_inclusive`](#new-is_inclusion_list_bits_inclusive) + + + +## Introduction + +This is the inclusion list specification for the fork-choice enforced inclusion +list feature. + +## Containers + +### New containers + +#### `InclusionListStore` + +```python +@dataclass +class InclusionListStore(object): + inclusion_lists: DefaultDict[Tuple[Slot, Root], Set[InclusionList]] = field( + default_factory=lambda: defaultdict(set) + ) + equivocators: DefaultDict[Tuple[Slot, Root], Set[ValidatorIndex]] = field( + default_factory=lambda: defaultdict(set) + ) +``` + +## Helpers + +### New `get_inclusion_list_store` + +```python +def get_inclusion_list_store() -> InclusionListStore: + # `cached_or_new_inclusion_list_store` is implementation and context dependent. + # It returns the cached `InclusionListStore`; if none exists, + # it initializes a new instance, caches it and returns it. + inclusion_list_store = cached_or_new_inclusion_list_store() + + return inclusion_list_store +``` + +### New `process_inclusion_list` + +```python +def process_inclusion_list( + store: InclusionListStore, inclusion_list: InclusionList, is_before_view_freeze_deadline: bool +) -> None: + key = (inclusion_list.slot, inclusion_list.inclusion_list_committee_root) + + # Ignore `inclusion_list` from equivocators. + if inclusion_list.validator_index in store.equivocators[key]: + return + + for stored_inclusion_list in store.inclusion_lists[key]: + if stored_inclusion_list.validator_index != inclusion_list.validator_index: + continue + + if stored_inclusion_list != inclusion_list: + store.equivocators[key].add(inclusion_list.validator_index) + store.inclusion_lists[key].remove(stored_inclusion_list) + + # Whether it was an equivocation or not, we have processed this `inclusion_list`. + return + + # Only store `inclusion_list` if it arrived before the view freeze deadline. + if is_before_view_freeze_deadline: + store.inclusion_lists[key].add(inclusion_list) +``` + +### New `get_inclusion_list_transactions` + +*Note*: `get_inclusion_list_transactions` returns a list of unique transactions +from all valid and non-equivocating `InclusionList`s that were received in a +timely manner on the p2p network for the given slot and for which the +`inclusion_list_committee_root` in the `InclusionList` matches the one +calculated based on the current state. + +```python +def get_inclusion_list_transactions( + store: InclusionListStore, state: BeaconState, slot: Slot +) -> Sequence[Transaction]: + inclusion_list_committee = get_inclusion_list_committee(state, slot) + inclusion_list_committee_root = hash_tree_root(inclusion_list_committee) + key = (slot, inclusion_list_committee_root) + + inclusion_list_transactions = [ + transaction + for inclusion_list in store.inclusion_lists[key] + if inclusion_list.validator_index not in store.equivocators[key] + for transaction in inclusion_list.transactions + ] + + # Deduplicate inclusion list transactions. Order does not need to be preserved. + return list(set(inclusion_list_transactions)) +``` + +### New `get_inclusion_list_bits` + +```python +def get_inclusion_list_bits( + store: InclusionListStore, state: BeaconState, slot: Slot +) -> Bitvector[INCLUSION_LIST_COMMITTEE_SIZE]: + inclusion_list_committee = get_inclusion_list_committee(state, slot) + inclusion_list_committee_root = hash_tree_root(inclusion_list_committee) + key = (slot, inclusion_list_committee_root) + + validator_indices = [ + inclusion_list.validator_index for inclusion_list in store.inclusion_lists[key] + ] + + return Bitvector[INCLUSION_LIST_COMMITTEE_SIZE]( + validator_index in validator_indices for validator_index in inclusion_list_committee + ) +``` + +### New `is_inclusion_list_bits_inclusive` + +```python +def is_inclusion_list_bits_inclusive( + store: InclusionListStore, + state: BeaconState, + slot: Slot, + inclusion_list_bits: Bitvector[INCLUSION_LIST_COMMITTEE_SIZE], +) -> bool: + inclusion_list_store = get_inclusion_list_store() + + local_inclusion_list_bits = get_inclusion_list_bits(inclusion_list_store, state, Slot(slot - 1)) + + return all( + inclusion_bit or not local_inclusion_bit + for inclusion_bit, local_inclusion_bit in zip( + inclusion_list_bits, local_inclusion_list_bits + ) + ) +``` diff --git a/specs/_features/eip7732/validator.md b/specs/_features/eip7732/validator.md index 2200221297..75170f3185 100644 --- a/specs/_features/eip7732/validator.md +++ b/specs/_features/eip7732/validator.md @@ -5,15 +5,31 @@ - [Introduction](#introduction) +- [Configuration](#configuration) + - [Time parameters](#time-parameters) +- [Helpers](#helpers) + - [New `GetInclusionListResponse`](#new-getinclusionlistresponse) +- [Protocols](#protocols) + - [`ExecutionEngine`](#executionengine) + - [New `get_inclusion_list`](#new-get_inclusion_list) - [Validator assignment](#validator-assignment) - - [Lookahead](#lookahead) + - [Payload timeliness committee](#payload-timeliness-committee) + - [Lookahead](#lookahead) + - [Inclusion list committee](#inclusion-list-committee) + - [Lookahead](#lookahead-1) - [Beacon chain responsibilities](#beacon-chain-responsibilities) - [Attestation](#attestation) + - [Attestation data](#attestation-data) + - [Modified LMD GHOST vote](#modified-lmd-ghost-vote) - [Sync Committee participations](#sync-committee-participations) + - [`get_sync_committee_message`](#get_sync_committee_message) + - [Modified `beacon_block_root`](#modified-beacon_block_root) - [Block proposal](#block-proposal) - [Constructing the new `signed_execution_payload_header` field in `BeaconBlockBody`](#constructing-the-new-signed_execution_payload_header-field-in-beaconblockbody) - [Constructing the new `payload_attestations` field in `BeaconBlockBody`](#constructing-the-new-payload_attestations-field-in-beaconblockbody) - [Blob sidecars](#blob-sidecars) + - [Inclusion list proposal](#inclusion-list-proposal) + - [Constructing the `SignedInclusionList`](#constructing-the-signedinclusionlist) - [Payload timeliness attestation](#payload-timeliness-attestation) - [Constructing a payload attestation](#constructing-a-payload-attestation) - [Modified functions](#modified-functions) @@ -26,8 +42,53 @@ This document represents the changes and additions to the Honest validator guide included in the EIP-7732 fork. +## Configuration + +### Time parameters + +| Name | Value | Unit | Duration | +| ------------------------------------ | --------------------------- | :-----: | :--------: | +| `INCLUSION_LIST_SUBMISSION_DEADLINE` | `SECONDS_PER_SLOT * 2 // 3` | seconds | 8 seconds | +| `PROPOSER_INCLUSION_LIST_CUTOFF` | `SECONDS_PER_SLOT - 1` | seconds | 11 seconds | + +## Helpers + +### New `GetInclusionListResponse` + +```python +@dataclass +class GetInclusionListResponse(object): + inclusion_list_transactions: Sequence[Transaction] +``` + +## Protocols + +### `ExecutionEngine` + +*Note*: `get_inclusion_list` function is added to the `ExecutionEngine` protocol +for use as an inclusion list committee member. + +The body of this function is implementation dependent. The Engine API may be +used to implement it with an external execution engine. + +#### New `get_inclusion_list` + +`get_inclusion_list` returns `GetInclusionListResponse` with the most recent +inclusion list transactions that has been built based on the latest view of the +public mempool. + +```python +def get_inclusion_list(self: ExecutionEngine) -> GetInclusionListResponse: + """ + Return ``GetInclusionListResponse`` object. + """ + ... +``` + ## Validator assignment +### Payload timeliness committee + A validator may be a member of the new Payload Timeliness Committee (PTC) for a given slot. To check for PTC assignments the validator uses the helper `get_ptc_assignment(state, epoch, validator_index)` where `epoch <= next_epoch`. @@ -53,7 +114,7 @@ def get_ptc_assignment( return None ``` -### Lookahead +#### Lookahead *[New in EIP7732]* @@ -61,6 +122,42 @@ def get_ptc_assignment( assignment for the next epoch (`current_epoch + 1`). A validator should plan for future assignments by noting their assigned PTC slot. +### Inclusion list committee + +A validator may be a member of the new inclusion list committee for a given +slot. To determine inclusion list committee assignments, the validator can run +the following function: +`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 inclusion list committee. 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 inclusion list +committee slot. + ## Beacon chain responsibilities All validator responsibilities remain unchanged other than the following: @@ -89,11 +186,33 @@ to signal the payload status of the block being attested to - Set `data.index = 1` to signal that the payload is present in the canonical chain (payload status is `FULL` in the fork-choice). +#### Attestation data + +*Note*: The only change to `attestation_data` is to call +`get_attester_head(store, head_root)` to set the `beacon_block_root` field of +`attestation_data`. + +##### Modified LMD GHOST vote + +Set `attestation_data.beacon_block_root = get_attester_head(store, head_root)`. + ### Sync Committee participations Sync committee duties are not changed for validators, however the submission deadline is implicitly changed by the change in `INTERVALS_PER_SLOT`. +*Note*: The only change to `get_sync_committee_message` is to call +`get_attester_head(store, head_root)` to set the `beacon_block_root` parameter +of `get_sync_committee_message`. + +#### `get_sync_committee_message` + +##### Modified `beacon_block_root` + +The `beacon_block_root` parameter MUST be set to return value of +[`get_attester_head(store: Store, head_root: Root)`](./fork-choice.md#new-get_attester_head) +function. + ### Block proposal Validators are still expected to propose `SignedBeaconBlock` at the beginning of @@ -144,6 +263,50 @@ construction is not necessary. This deprecates the corresponding sections from the honest validator guide in the Electra fork, moving them, albeit with some modifications, to the [honest Builder guide](./builder.md) +### Inclusion list proposal + +A validator is expected to propose a +[`SignedInclusionList`](./beacon-chain.md#signedinclusionlist) at the beginning +of any `slot` for which +`get_inclusion_committee_assignment(state, epoch, validator_index)` returns. + +If a validator is in the current inclusion list committee, the validator should +create and broadcast the `signed_inclusion_list` to the global `inclusion_list` +subnet by `INCLUSION_LIST_SUBMISSION_DEADLINE` seconds into the slot after +processing the block for the current slot and confirming it as the head. If no +block is received by `INCLUSION_LIST_SUBMISSION_DEADLINE - 1` seconds into the +slot, the validator should run `get_head` to determine the local head and +construct and broadcast the inclusion list based on this local head by +`INCLUSION_LIST_SUBMISSION_DEADLINE` seconds into the slot. + +#### Constructing the `SignedInclusionList` + +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 `ExecutionEngine` + via `get_inclusion_list`. +- 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) +``` + ### Payload timeliness attestation Some validators are selected to submit payload timeliness attestations. @@ -205,6 +368,19 @@ def get_payload_attestation_message_signature( *Note*: The function `prepare_execution_payload` is modified to handle the updated `get_expected_withdrawals` return signature. +*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 to `prepare_execution_payload` is to call +`get_inclusion_list_store` and `get_inclusion_list_transactions` to set the new +`inclusion_list_transactions` field of `PayloadAttributes`. + +*Note*: A proposer should produce an execution payload that satisfies the +inclusion list constraints with respect to the inclusion lists gathered up to +`PROPOSER_INCLUSION_LIST_CUTOFF` into the slot. + ```python def prepare_execution_payload( state: BeaconState, @@ -220,12 +396,20 @@ def prepare_execution_payload( # Set the forkchoice head and initiate the payload build process withdrawals, _, _ = get_expected_withdrawals(state) + # [New in EIP7805] + # Get the inclusion list store + inclusion_list_store = get_inclusion_list_store() + payload_attributes = PayloadAttributes( timestamp=compute_time_at_slot(state, state.slot), prev_randao=get_randao_mix(state, get_current_epoch(state)), suggested_fee_recipient=suggested_fee_recipient, withdrawals=withdrawals, parent_beacon_block_root=hash_tree_root(state.latest_block_header), + # [New in EIP7805] + inclusion_list_transactions=get_inclusion_list_transactions( + inclusion_list_store, state, Slot(state.slot - 1) + ), ) return execution_engine.notify_forkchoice_updated( head_block_hash=parent_hash,