# ePBS Annotated Validator Spec ### ExecutionEngine changes #### New addition: `get_inclusion_list` `get_inclusion_list` retrieves a list of `transactions` and their corresponding `summaries` from an execution layer client using the Execution Engine API. The function's implementation may vary, similar to the implementation [`get_payload` ](https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/validator.md#get_payload)It interfaces with an external execution engine via the Engine API. The function returns a `GetInclusionListResponse` object, which includes both `summaries` and `transactions`. The `summaries` field consists of a list of execution addresses from the transaction senders, while `transactions` field consists of a list of transactions. Both the summaries and transactions are of equal length, summaries is ordered by `from` addresses of transactions, and are limited by `MAX_TRANSACTIONS_PER_INCLUSION_LIST`. The function's input is `parent_block_hash`, which determines the list of transactions. These transactions **must** be valid from the perspective of `parent_block_hash`. In terms of validity conditions, refer to the [validation steps](https://hackmd.io/@potuz/rJ9GCnT1C#EL-validation) from the overall spec notes. The CL client fully trusts their local EL client for these inclusion list transactions and should not have to verify the transactions, similar to proposing execution payload locally. ```python class GetInclusionListResponse(container) summary: List[ExecutionAddress, MAX_TRANSACTIONS_PER_INCLUSION_LIST] transactions: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] ``` ```python def get_inclusion_list(self: ExecutionEngine, parent_block_hash: Root) -> GetInclusionListResponse: """ Return ``GetInclusionListResponse`` object. """ ... ``` `get_execution_inclusion_list` can be called in advance of the slot start if the execution state for the slot is certain. If the validator begins preparing a local block on top of an empty block, broadcasting IL may not be necessary. But it is always permissible to broadcast an IL anyway to be safe and keep the implementation simple. Rational validators are incentivized to broadcast their inclusion lists early to maximize their chances of having their blocks seen by the majority and attested. More to that under the Proposer inclusion list proposal section. ### Validator assignment This section explains how a validator determines whether it performs PTC duty in the current or next epoch. Similar to determining if it is responsible for submitting beacon attestations during a slot across an epoch, PTC attestation is a subset type where the PTC committee is selected by choosing a few validators from the end of each beacon attestation committee. ```python def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]: """ Get the ptc committee for the given ``slot`` """ epoch = compute_epoch_at_slot(slot) committees_per_slot = bit_floor(min(get_committee_count_per_slot(state, epoch), PTC_SIZE)) members_per_committee = PTC_SIZE // committees_per_slot validator_indices = [] for idx in range(committees_per_slot): beacon_committee = get_beacon_committee(state, slot, idx) validator_indices += beacon_committee[:members_per_committee] return validator_indices ``` Retrieving the assignment utilizing `get_ptc` through requested `epoch` and `validator_index` parameters. If the validator exists in the PTC then return the assignment slot is returned otherwise, `false` is returned. `get_ptc_assignment` should be called at the start of each epoch to get the assignment for the current and next epoch (`current_epoch + 1`). A validator should plan for future assignments by noting their assigned PTC committee slot and planning to participate in the PTC gossip subnet. ```python def get_ptc_assignment(state: BeaconState, epoch: Epoch, validator_index: ValidatorIndex ) -> Optional[Slot]: 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_ptc(state, Slot(slot)): return Slot(slot) return None ``` ## Beacon chain responsibilities This section explains validator duty changes under ePBS. We'll divide them into the following subsections 1. **Modified**: proposer block duty 2. **New**: proposer inclusion list duty 3. **New**: PTC attester duty ### Proposer block duty At the beginning of the slot, the proposer assigned for that slot prepares and broadcasts a `SignedBeaconBlock`. The high-level modifications are as follows: - Instead of including an `ExecutionPayload` on block body, the validator includes `SignedExecutionPayloadHeader` in the block body. The details of how to obtain the header is outside the scope of this document. A proposer can obtain the header through local buildings, p2p gossip networks, or other avenues like the builder's RPC ports. Strategies for dealing with the header will be discussed in the following proposer block header selection section. - `BlobSidecars` is no longer part of the block body. This duty is transferred to the builders. - `PayloadAttestation` is added to the block body. It consists of the aggregate of the `PayloadAttestationData`. The strategy for honest aggregation will be defined in the PTC attestation aggregation section. #### Proposer block header selection The proposer selects the `SignedExecutionPayloadHeader` to be included in the block. The header can be trusted by the local EL client or untrusted by P2P network or builder RPC interface. If the header comes from a trusted source, the proposer can include it as is and broadcast both `SignedBeaconBlock` and `SignedExecutionPayloadEnvelope` in the respective gossip nets simultaneously. If the header is coming from an untrusted source, the proposer should verify the header is valid before including it in their block. The header must pass the validation steps outlined in `process_execution_payload_header`, which are: ```python def validate_execution_payload_header(state: BeaconState, signed_header: `SignedExecutionPayloadHeader`) -> None: assert verify_execution_payload_header_signature(state, signed_header) # Check that the builder has funds to cover the bid header = signed_header.message builder_index = header.builder_index amount = header.value assert state.balances[builder_index] >= amount # Verify that the bid is for the current slot assert header.slot == block.slot # Verify that the bid is for the right parent block assert header.parent_block_hash == state.latest_block_hash assert header.parent_block_root == block.parent_root ``` If the header is sourced from a p2p network, the p2p gossip validation pipeline has likely already performed the necessary checks, so there is no need to validate it again. However, if the header is obtained through builder RPC, the check should be performed separately to ensure its validity before inclusion in the block. #### Proposer PTC attestation aggregation The proposer aggregates recently verified `PayloadAttestationMessage` into `PayloadAttestation` for inclusion in the block body. Up to `MAX_PAYLOAD_ATTESTATIONS` can be included. The honest PTC selection and aggregation strategy are defined as follows: - Advance head state `state` to the proposal slot. - The proposer retrieves all `PayloadAttestationMessage` from the local mempool where `data.slot + 1 == state.slot`. It is assumed that the PTC attestations have been verified as they came through the P2P gossip network. - The proposer determines which `payload_status` to include by checking if the payload was supposed to be present in the previous slot using `state.latest_full_slot == slot - 1`. If the payload was supposed to be present, it filters for `PayloadAttestationMessage` where `data.payload_status == PAYLOAD_PRESENT`. If the payload is not supposed to be present, it filters for `data.payload_status == PAYLOAD_ABSENT || data.payload_status == PAYLOAD_WITHHELD`. - The proposer now has all the payload attestations that satisfy the slot and payload status requirements. It aggregates these attestations similarly to how beacon attestations are aggregated. It fills the `payload_attestation.aggregation_bit` s field using the relative position of the validator indices with respect to the PTC obtained from `get_ptc(state, slot—1)``. - Finally, the proposer fills the `payload_attestation.signature` field with the aggregated signature and includes it in the beacon block. > **Note:** > 1. Including a payload status that diverges from the canonical chain's view will result in the proposer being penalized by `2*proposer_penalty_numerator // proposer_reward_denominator` per PTC attestation. > 2. In the beacon chain's state transition, the payload attestation processing function will unset the flags if they were set by an equivocating PTC attestation. The justification for this approach is that there are no slashing conditions for equivocations on the PTC attestations. Attesters are incentivized to submit both `PAYLOAD_PRESENT` and `PAYLOAD_ABSENT` attestations, and proposers are incentivized to include both, allowing them to be rewarded in either case. #### Proposer inclusion list proposal The proposer calls `get_inclusion_list` at the start of the slot, or even earlier as soon as the execution state is known. Based on the `GetInclusionListResponse`, the proposer can construct the `InclusionList` using the following example helpers. ```python def get_inclusion_list_summary(slot: Slot, proposer_index: ValidatorIndex, response: GetInclusionListResponse) -> InclusionListSummary: return InclusionListSummary(proposer_index=proposer_index, slot=slot, summary=response.summary) def get_inclusion_list_summary_signature(state: BeaconState, inclusion_list_summary: InclusionListSummary, privkey: int) -> BLSSignature: domain = get_domain(state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(state.slot)) signing_root = compute_signing_root(inclusion_list_summary, domain) return bls.Sign(privkey, signing_root) def get_inclusion_list(signed_summary: SignedInclusionListSummary, parent_block_hash: Hash32, response: GetInclusionListResponse) -> InclusionList: return InclusionList(signed_summary=signed_summary, parent_block_hash=parent_block_hash, transactions=response.transactions) ``` In terms of **when** or **whether** to propose an inclusion list, there are a few variables to monitor: consensus block reveal status, execution payload reveal status, and PTC attestation votes. The honest strategy involves waiting until the start of the slot and then calling `is_parent_block_full`. If it returns `False`, the proposer does not have to broadcast an inclusion list, as honest beacon nodes will ignore any inclusion list. The proposer may act earlier if there is strong confidence in the execution reveal and PTC vote status, outlined in the following table. |Consensus block|Execution Payload|PTC vote result|Action| | - | - | - | - | | seen | seen | >50% voted present| Use `state.latest_block_hash` | | seen | not seen | >50% voted absent | do nothing | | seen | seen and it's empty | >50% voted withhold | once there is a `pre_state` for proposal, advance to `proposal_slot` and call `is_parent_full`. | | seen | seen | Not enough vote to conclude | Wait until something reaches 50% or call `is_parent_block_full` at the start of the slot | | seen | not seen | >50% voted present | Request payload over p2p RPC | | not seen | not seen | n/a | Check `is_parent_block_full` before action | ### PTC attestation duty Suppose the attester is part of the PTC, as determined by `get_ptc_assignment` for your validator index in any of the slots of the current or next epoch. In that case, the PTC attester should prepare and broadcast a `PayloadAttestationMessage` at the `SECONDS_PER_SLOT * 3 / INTERVALS_PER_SLOT` mark of the slot. The `PayloadAttestationData` includes the `beacon_block_root`, which is the block root of the current slot, and the `slot`, which refers to the current slot. The `payload_status` is defined as follows. |Consensus block status |Execution payload status |Payload status | | - | - | - | | seen | on time | `PAYLOAD_PRESENT` | | seen | late | `PAYLOAD_ABSENT` | | seen | withhold | `PAYLOAD_WITHHOLD` | | late | on time | `PAYLOAD_PRESENT` | | late | late | `PAYLOAD_ABSENT` | | late | withhold | `PAYLOAD_WITHHOLD` | | skipped | skipped | do nothing | Assuming you have `PayloadAttestationData`, you can construct a `PayloadAttestationMessage` by signing over the `PayloadAttestationData`. Then, input `validator_index` as your index and `signature` as the result of the `get_payload_attestation_data_signature`. ```python def get_payload_attestation_data_signature(state: BeaconState, data: PayloadAttestationData, privkey: int) -> BLSSignature: domain = get_domain(state, DOMAIN_PTC_ATTESTER, compute_epoch_at_slot(data.slot)) signing_root = compute_signing_root(data, domain) return bls.Sign(privkey, signing_root) ``` ### No change to attester duty If the validator, assumed to be an attester of a slot, is also part of the PTC, one might argue that attester duty can be skipped since the reward will not be counted. However, for implementation simplicity, it is recommended to just gossip the attestation anyway, as there is no penalty, and signing and verifying the signatures are inexpensive. The benefit is that the attestation's fork choice weight remains useful over P2P.