diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md new file mode 100644 index 0000000000..22f10de405 --- /dev/null +++ b/specs/_features/epbs/beacon-chain.md @@ -0,0 +1,731 @@ +# ePBS -- The Beacon Chain + +## Table of contents + + + + + +- [Introduction](#introduction) +- [Constants](#constants) + - [Payload status](#payload-status) +- [Preset](#preset) + - [Misc](#misc) + - [Domain types](#domain-types) + - [Time parameters](#time-parameters) + - [Max operations per block](#max-operations-per-block) + - [Execution](#execution) +- [Containers](#containers) + - [New containers](#new-containers) + - [`PayloadAttestationData`](#payloadattestationdata) + - [`PayloadAttestation`](#payloadattestation) + - [`PayloadAttestationMessage`](#payloadattestationmessage) + - [`IndexedPayloadAttestation`](#indexedpayloadattestation) + - [`SignedExecutionPayloadHeader`](#signedexecutionpayloadheader) + - [`ExecutionPayloadEnvelope`](#executionpayloadenvelope) + - [`SignedExecutionPayloadEnvelope`](#signedexecutionpayloadenvelope) + - [`InclusionListSummary`](#inclusionlistsummary) + - [`SignedInclusionListSummary`](#signedinclusionlistsummary) + - [`InclusionList`](#inclusionlist) + - [Modified containers](#modified-containers) + - [`BeaconBlockBody`](#beaconblockbody) + - [`ExecutionPayload`](#executionpayload) + - [`ExecutionPayloadHeader`](#executionpayloadheader) + - [`BeaconState`](#beaconstate) +- [Helper functions](#helper-functions) + - [Math](#math) + - [`bit_floor`](#bit_floor) + - [Predicates](#predicates) + - [`is_valid_indexed_payload_attestation`](#is_valid_indexed_payload_attestation) + - [`is_parent_block_full`](#is_parent_block_full) + - [Beacon State accessors](#beacon-state-accessors) + - [`get_ptc`](#get_ptc) + - [`get_payload_attesting_indices`](#get_payload_attesting_indices) + - [`get_indexed_payload_attestation`](#get_indexed_payload_attestation) +- [Beacon chain state transition function](#beacon-chain-state-transition-function) + - [Block processing](#block-processing) + - [Modified `process_withdrawals`](#modified-process_withdrawals) + - [New `verify_execution_payload_header_signature`](#new-verify_execution_payload_header_signature) + - [New `process_execution_payload_header`](#new-process_execution_payload_header) + - [Modified `process_operations`](#modified-process_operations) + - [Modified `process_attestation`](#modified-process_attestation) + - [Payload Attestations](#payload-attestations) + - [New `verify_execution_payload_envelope_signature`](#new-verify_execution_payload_envelope_signature) + - [New `verify_inclusion_list_summary_signature`](#new-verify_inclusion_list_summary_signature) + - [Modified `process_execution_payload`](#modified-process_execution_payload) + + + + +## Introduction + +This is the beacon chain specification of the enshrined proposer builder separation feature. + +*Note:* This specification is built upon [Deneb](../../deneb/beacon-chain.md) and is under active development. + +This feature adds new staked consensus participants called *Builders* and new honest validators duties called *payload timeliness attestations*. The slot is divided in **four** intervals, one more added to the current three. Honest validators gather *signed bids* from builders and submit their consensus blocks (a `SignedBeaconBlock`) at the beginning of the slot. At the start of the second interval, honest validators submit attestations just as they do previous to this feature). At the start of the third interval, aggregators aggregate these attestations (exactly as before this feature) and the honest builder reveals the full payload. At the start of the fourth interval, some honest validators selected to be members of the new **Payload Timeliness Committee** attest to the presence of the builder's payload. + +At any given slot, the status of the blockchain's head may be either +- A block from a previous slot (e.g. the current slot's proposer did not submit its block). +- An *empty* block from the current slot (e.g. the proposer submitted a timely block, but the builder did not reveal the payload on time). +- A full block for the current slot (both the proposer and the builder revealed on time). + +## Constants + +### Payload status + +| Name | Value | +| - | - | +| `PAYLOAD_ABSENT` | `uint8(0)` | +| `PAYLOAD_PRESENT` | `uint8(1)` | +| `PAYLOAD_WITHHELD` | `uint8(2)` | +| `PAYLOAD_INVALID_STATUS` | `uint8(3)` | + +## Preset + +### Misc + +| Name | Value | +| - | - | +| `PTC_SIZE` | `uint64(2**9)` (=512) # (New in ePBS) | + +### Domain types + +| Name | Value | +| - | - | +| `DOMAIN_BEACON_BUILDER` | `DomainType('0x1B000000')` # (New in ePBS)| +| `DOMAIN_PTC_ATTESTER` | `DomainType('0x0C000000')` # (New in ePBS)| + +### Time parameters + +| Name | Value | Unit | Duration | +| - | - | :-: | :-: | +| `MIN_SLOTS_FOR_INCLUSION_LISTS_REQUESTS` | `uint64(2)` | slots | 24 seconds # (New in ePBS) | + +### Max operations per block + +| Name | Value | +| - | - | +| `MAX_PAYLOAD_ATTESTATIONS` | `2**2` (= 4) # (New in ePBS) | + +### Execution +| Name | Value | +| - | - | +| MAX_TRANSACTIONS_PER_INCLUSION_LIST | `2**10` (=1024) # (New in ePBS) | + +## Containers + +### New containers + +#### `PayloadAttestationData` + +```python +class PayloadAttestationData(Container): + beacon_block_root: Root + slot: Slot + payload_status: uint8 +``` + +#### `PayloadAttestation` + +```python +class PayloadAttestation(Container): + aggregation_bits: BitVector[PTC_SIZE] + data: PayloadAttestationData + signature: BLSSignature +``` + +#### `PayloadAttestationMessage` + +```python +class PayloadAttestationMessage(Container): + validator_index: ValidatorIndex + data: PayloadAttestationData + signature: BLSSignature +``` + +#### `IndexedPayloadAttestation` + +```python +class IndexedPayloadAttestation(Container): + attesting_indices: List[ValidatorIndex, PTC_SIZE] + data: PayloadAttestationData + signature: BLSSignature +``` + +#### `SignedExecutionPayloadHeader` + +```python +class SignedExecutionPayloadHeader(Container): + message: ExecutionPayloadHeader + signature: BLSSignature +``` + +#### `ExecutionPayloadEnvelope` + +```python +class ExecutionPayloadEnvelope(Container): + payload: ExecutionPayload + builder_index: ValidatorIndex + beacon_block_root: Root + blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + inclusion_list_proposer_index: ValidatorIndex + inclusion_list_slot: Slot + inclusion_list_signature: BLSSignature + payload_withheld: bool + state_root: Root +``` + +#### `SignedExecutionPayloadEnvelope` + +```python +class SignedExecutionPayloadEnvelope(Container): + message: ExecutionPayloadEnvelope + signature: BLSSignature +``` + +#### `InclusionListSummary` + +```python +class InclusionListSummary(Container) + proposer_index: ValidatorIndex + slot: Slot + summary: List[ExecutionAddress, MAX_TRANSACTIONS_PER_INCLUSION_LIST] +``` + +#### `SignedInclusionListSummary` + +```python +class SignedInclusionListSummary(Container): + message: InclusionListSummary + signature: BLSSignature +``` + +#### `InclusionList` + +```python +class InclusionList(Container) + signed_summary: SignedInclusionListSummary + parent_block_hash: Hash32 + transactions: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] +``` + +### Modified containers + + +#### `BeaconBlockBody` +**Note:** The Beacon Block body is modified to contain a Signed `ExecutionPayloadHeader`. The containers `BeaconBlock` and `SignedBeaconBlock` are modified indirectly. + +```python +class BeaconBlockBody(Container): + randao_reveal: BLSSignature + eth1_data: Eth1Data # Eth1 data vote + graffiti: Bytes32 # Arbitrary data + # Operations + proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS] + attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS] + attestations: List[Attestation, MAX_ATTESTATIONS] + deposits: List[Deposit, MAX_DEPOSITS] + voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS] + sync_aggregate: SyncAggregate + # Execution + # Removed execution_payload [Removed in ePBS] + # Removed blob_kzg_commitments [Removed in ePBS] + bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] + # PBS + signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] + payload_attestations: List[PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS] # [New in ePBS] +``` + +#### `ExecutionPayload` + +**Note:** The `ExecutionPayload` is modified to contain a transaction inclusion list summary signed by the corresponding beacon block proposer. + +```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] + blob_gas_used: uint64 + excess_blob_gas: uint64 + inclusion_list_summary: List[ExecutionAddress, MAX_TRANSACTIONS_PER_INCLUSION_LIST]# [New in ePBS] +``` + +#### `ExecutionPayloadHeader` + +**Note:** The `ExecutionPayloadHeader` is modified to only contain the block hash of the committed `ExecutionPayload` in addition to the builder's payment information and KZG commitments root to verify the inclusion proofs. + +```python +class ExecutionPayloadHeader(Container): + parent_block_hash: Hash32 + parent_block_root: Root + block_hash: Hash32 + builder_index: ValidatorIndex + slot: Slot + value: Gwei + blob_kzg_commitments_root: Root +``` + +#### `BeaconState` +*Note*: the beacon state is modified to store a signed latest execution payload header, and to track the last withdrawals honored in the CL. It is also modified to no longer store the full last execution payload header but rather only the last block hash and the last slot that was full, that is in which there were both consensus and execution blocks included. + +```python +class BeaconState(Container): + # Versioning + genesis_time: uint64 + genesis_validators_root: Root + slot: Slot + fork: Fork + # History + latest_block_header: BeaconBlockHeader + block_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + state_roots: Vector[Root, SLOTS_PER_HISTORICAL_ROOT] + historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT] # Frozen in Capella, replaced by historical_summaries + # Eth1 + eth1_data: Eth1Data + eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] + eth1_deposit_index: uint64 + # Registry + validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] + balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] + # Randomness + randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR] + # Slashings + slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] # Per-epoch sums of slashed effective balances + # Participation + previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + # Finality + justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] # Bit set for every recent justified epoch + previous_justified_checkpoint: Checkpoint + current_justified_checkpoint: Checkpoint + finalized_checkpoint: Checkpoint + # Inactivity + inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT] + # Sync + current_sync_committee: SyncCommittee + next_sync_committee: SyncCommittee + # Execution + # latest_execution_payload_header: ExecutionPayloadHeader # [Removed in ePBS] + execution_payload_header: ExecutionPayloadHeader # [New in ePBS] + # Withdrawals + next_withdrawal_index: WithdrawalIndex + next_withdrawal_validator_index: ValidatorIndex + # Deep history valid from Capella onwards + historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] + # PBS + previous_inclusion_list_proposer: ValidatorIndex # [New in ePBS] + previous_inclusion_list_slot: Slot # [New in ePBS] + latest_inclusion_list_proposer: ValidatorIndex # [New in ePBS] + latest_inclusion_list_slot: Slot # [New in ePBS] + latest_block_hash: Hash32 # [New in ePBS] + latest_full_slot: Slot # [New in ePBS] + last_withdrawals_root: Root # [New in ePBS] +``` + +## Helper functions + +### Math + +#### `bit_floor` + +```python +def bit_floor(n: uint64) -> uint64: + """ + if ``n`` is not zero, returns the largest power of `2` that is not greater than `n`. + """ + if n == 0: + return 0 + return uint64(1) << (n.bit_length() - 1) +``` + +### Predicates + +#### `is_valid_indexed_payload_attestation` + +```python +def is_valid_indexed_payload_attestation(state: BeaconState, indexed_payload_attestation: IndexedPayloadAttestation) -> bool: + """ + Check if ``indexed_payload_attestation`` is not empty, has sorted and unique indices and has a valid aggregate signature. + """ + # Verify the data is valid + if indexed_payload_attestation.data.payload_status >= PAYLOAD_INVALID_STATUS: + return False + + # Verify indices are sorted and unique + indices = indexed_payload_attestation.attesting_indices + if len(indices) == 0 or not indices == sorted(set(indices)): + return False + # Verify aggregate signature + pubkeys = [state.validators[i].pubkey for i in indices] + domain = get_domain(state, DOMAIN_PTC_ATTESTER, None) + signing_root = compute_signing_root(indexed_payload_attestation.data, domain) + return bls.FastAggregateVerify(pubkeys, signing_root, indexed_payload_attestation.signature) +``` + +#### `is_parent_block_full` + +This function returns true if the last committed payload header was fulfilled with a payload, this can only happen when both beacon block and payload were present. This function must be called on a beacon state before processing the execution payload header in the block. + +```python +def is_parent_block_full(state: BeaconState) -> bool: + return state.execution_payload_header.block_hash == state.latest_block_hash +``` + +### Beacon State accessors + +#### `get_ptc` + +```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 +``` + +#### `get_payload_attesting_indices` + +```python +def get_payload_attesting_indices(state: BeaconState, slot: Slot, + payload_attestation: PayloadAttestation) -> Set[ValidatorIndex]: + """ + Return the set of attesting indices corresponding to ``payload_attestation``. + """ + ptc = get_ptc(state, slot) + return set(index for i, index in enumerate(ptc) if payload_attestation.aggregation_bits[i]) +``` + + +#### `get_indexed_payload_attestation` + +```python +def get_indexed_payload_attestation(state: BeaconState, slot: Slot, + payload_attestation: PayloadAttestation) -> IndexedPayloadAttestation: + """ + Return the indexed payload attestation corresponding to ``payload_attestation``. + """ + attesting_indices = get_payload_attesting_indices(state, slot, payload_attestation) + + return IndexedPayloadAttestation( + attesting_indices=sorted(attesting_indices), + data=payload_attestation.data, + signature=payload_attestation.signature, + ) +``` + +## Beacon chain state transition function + +*Note*: state transition is fundamentally modified in ePBS. The full state transition is broken in two parts, first importing a signed block and then importing an execution payload. + +The post-state corresponding to a pre-state `state` and a signed block `signed_block` is defined as `state_transition(state, signed_block)`. State transitions that trigger an unhandled exception (e.g. a failed `assert` or an out-of-range list access) are considered invalid. State transitions that cause a `uint64` overflow or underflow are also considered invalid. + +The post-state corresponding to a pre-state `state` and a signed execution payload `signed_execution_payload` is defined as `process_execution_payload(state, signed_execution_payload)`. State transitions that trigger an unhandled exception (e.g. a failed `assert` or an out-of-range list access) are considered invalid. State transitions that cause a `uint64` overflow or underflow are also considered invalid. + +### Block processing + +*Note*: the function `process_block` is modified to only process the consensus block. The full state-transition process is broken into separate functions, one to process a `BeaconBlock` and another to process a `SignedExecutionPayload`. Notice that withdrawals are now included in the beacon block, they are processed before the execution payload header as this header may affect validator balances. + + +```python +def process_block(state: BeaconState, block: BeaconBlock) -> None: + process_block_header(state, block) + process_withdrawals(state) [Modified in ePBS] + process_execution_payload_header(state, block) # [Modified in ePBS, removed process_execution_payload] + process_randao(state, block.body) + process_eth1_data(state, block.body) + process_operations(state, block.body) # [Modified in ePBS] + process_sync_aggregate(state, block.body.sync_aggregate) +``` + +#### Modified `process_withdrawals` +**Note:** This is modified to take only the `state` parameter. The payload is required to honor these withdrawals. + +```python +def process_withdrawals(state: BeaconState) -> None: + ## return early if the parent block was empty + if not is_parent_block_full(state): + return + + withdrawals = get_expected_withdrawals(state) + state.last_withdrawals_root = hash_tree_root(withdrawals) + for withdrawal in withdrawals: + decrease_balance(state, withdrawal.validator_index, withdrawal.amount) + + # Update the next withdrawal index if this block contained withdrawals + if len(withdrawals) != 0: + latest_withdrawal = withdrawals[-1] + state.next_withdrawal_index = WithdrawalIndex(latest_withdrawal.index + 1) + + # Update the next validator index to start the next withdrawal sweep + if len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD: + # Next sweep starts after the latest withdrawal's validator index + next_validator_index = ValidatorIndex((withdrawals[-1].validator_index + 1) % len(state.validators)) + state.next_withdrawal_validator_index = next_validator_index + else: + # Advance sweep by the max length of the sweep if there was not a full set of withdrawals + next_index = state.next_withdrawal_validator_index + MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP + next_validator_index = ValidatorIndex(next_index % len(state.validators)) + state.next_withdrawal_validator_index = next_validator_index +``` + +#### New `verify_execution_payload_header_signature` + +```python +def verify_execution_payload_header_signature(state: BeaconState, + signed_header: SignedExecutionPayloadHeader) -> bool: + # Check the signature + builder = state.validators[signed_header.message.builder_index] + signing_root = compute_signing_root(signed_header.message, get_domain(state, DOMAIN_BEACON_BUILDER)) + return bls.Verify(builder.pubkey, signing_root, signed_header.signature) +``` + +#### New `process_execution_payload_header` + +```python +def process_execution_payload_header(state: BeaconState, block: BeaconBlock) -> None: + # Verify the header signature + signed_header = block.body.signed_execution_payload_header + 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 + + # Transfer the funds from the builder to the proposer + decrease_balance(state, builder_index, amount) + increase_balance(state, block.proposer_index, amount) + + # Cache the inclusion list proposer if the parent block was full + if is_parent_block_full(state): + state.latest_inclusion_list_proposer = block.proposer_index + state.latest_inclusion_list_slot = block.slot + # Cache the signed execution payload header + state.execution_payload_header = header +``` + + +#### Modified `process_operations` + +**Note:** `process_operations` is modified to process PTC attestations + +```python +def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: + # Verify that outstanding deposits are processed up to the maximum number of deposits + assert len(body.deposits) == min(MAX_DEPOSITS, state.eth1_data.deposit_count - state.eth1_deposit_index) + + def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: + for operation in operations: + fn(state, operation) + + for_ops(body.proposer_slashings, process_proposer_slashing) + for_ops(body.attester_slashings, process_attester_slashing) + for_ops(body.attestations, process_attestation) # [Modified in ePBS] + for_ops(body.deposits, process_deposit) + for_ops(body.voluntary_exits, process_voluntary_exit) + for_ops(body.bls_to_execution_changes, process_bls_to_execution_change) + for_ops(body.payload_attestations, process_payload_attestation) # [New in ePBS] +``` + +##### Modified `process_attestation` + +*Note*: The function `process_attestation` is modified to ignore attestations from the ptc + +```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 <= data.slot + SLOTS_PER_EPOCH + 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 + + ptc = get_ptc(state, data.slot) + attesting_indices = [i for i in get_attesting_indices(state, data, attestation.aggregation_bits) if i not in ptc] + proposer_reward_numerator = 0 + for index in attesting_indices: + 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) +``` + +##### Payload Attestations + +```python +def remove_flag(flags: ParticipationFlags, flag_index: int) -> ParticipationFlags: + flag = ParticipationFlags(2**flag_index) + return flags & ~flag +``` + +```python +def process_payload_attestation(state: BeaconState, payload_attestation: PayloadAttestation) -> None: + ## Check that the attestation is for the parent beacon block + data = payload_attestation.data + assert data.beacon_block_root == state.latest_block_header.parent_root + ## Check that the attestation is for the previous slot + assert data.slot + 1 == state.slot + + #Verify signature + indexed_payload_attestation = get_indexed_payload_attestation(state, data.slot, payload_attestation) + assert is_valid_indexed_payload_attestation(state, indexed_payload_attestation) + + ptc = get_ptc(state, data.slot) + if state.slot % SLOTS_PER_EPOCH == 0: + epoch_participation = state.previous_epoch_participation + else: + epoch_participation = state.current_epoch_participation + + # Return early if the attestation is for the wrong payload status + payload_was_present = data.slot == state.latest_full_slot + voted_present = data.payload_status == PAYLOAD_PRESENT + proposer_reward_denominator = (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT) * WEIGHT_DENOMINATOR // PROPOSER_WEIGHT + proposer_index = get_beacon_proposer_index(state) + if voted_present != payload_was_present: + # Unset the flags in case they were set by an equivocating ptc attestation + proposer_penalty_numerator = 0 + for index in indexed_payload_attestation.attesting_indices: + for flag_index, weight in enumerate(PARTICIPATION_FLAG_WEIGHTS): + if has_flag(epoch_participation[index], flag_index): + epoch_participation[index] = remove_flag(epoch_participation[index], flag_index) + proposer_penalty_numerator += get_base_reward(state, index) * weight + # Penalize the proposer + proposer_penalty = Gwei(2*proposer_penalty_numerator // proposer_reward_denominator) + decrease_balance(state, proposer_index, proposer_penalty) + return + + # Reward the proposer and set all the participation flags in case of correct attestations + proposer_reward_numerator = 0 + for index in indexed_payload_attestation.attesting_indices: + for flag_index, weight in enumerate(PARTICIPATION_FLAG_WEIGHTS): + if 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 = Gwei(proposer_reward_numerator // proposer_reward_denominator) + increase_balance(state, proposer_index, proposer_reward) +``` + +#### New `verify_execution_payload_envelope_signature` + +```python +def verify_execution_payload_envelope_signature(state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope) -> bool: + builder = state.validators[signed_envelope.message.builder_index] + signing_root = compute_signing_root(signed_envelope.message, get_domain(state, DOMAIN_BEACON_BUILDER)) + return bls.Verify(builder.pubkey, signing_root, signed_envelope.signature) +``` + +#### New `verify_inclusion_list_summary_signature` + +```python +def verify_inclusion_list_summary_signature(state: BeaconState, signed_summary: SignedInclusionListSummary) -> bool: + summary = signed_summary.message + signing_root = compute_signing_root(summary, get_domain(state, DOMAIN_BEACON_PROPOSER)) + proposer = state.validators[summary.proposer_index] + return bls.Verify(proposer.pubkey, signing_root, signed_summary.signature) +``` + +#### Modified `process_execution_payload` +*Note*: `process_execution_payload` is now an independent check in state transition. It is called when importing a signed execution payload proposed by the builder of the current slot. + +```python +def process_execution_payload(state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope, execution_engine: ExecutionEngine, verify = True) -> None: + # Verify signature + if verify: + assert verify_execution_payload_envelope_signature(state, signed_envelope) + envelope = signed_envelope.message + payload = envelope.payload + # Verify the withdrawals root + assert hash_tree_root(payload.withdrawals) == state.last_withdrawals_root + # Verify inclusion list proposer and slot + assert envelope.inclusion_list_proposer_index == state.previous_inclusion_list_proposer + assert envelope.inclusion_list_slot == state.previous_inclusion_list_slot + # Verify inclusion list summary signature + signed_summary = SignedInclusionListSummary( + message=InclusionListSummary( + proposer_index=envelope.inclusion_list_proposer_index, + slot = envelope.inclusion_list_slot, + summary=payload.inclusion_list_summary), + signature=envelope.inclusion_list_signature) + assert verify_inclusion_list_summary_signature(state, signed_summary) + # Verify consistency with the beacon block + assert envelope.beacon_block_root == hash_tree_root(state.latest_block_header) + # Verify consistency with the committed header + committed_header = state.execution_payload_header + assert committed_header.block_hash == payload.block_hash + assert committed_header.blob_kzg_commitments_root == hash_tree_root(envelope.blob_kzg_commitments) + assert envelope.builder_index == committed_header.builder_index + # Verify consistency of the parent hash with respect to the previous execution payload + assert payload.parent_hash == state.latest_block_hash + # Verify prev_randao + assert payload.prev_randao == get_randao_mix(state, get_current_epoch(state)) + # Verify timestamp + assert payload.timestamp == compute_timestamp_at_slot(state, state.slot) + # Verify the execution payload is valid + versioned_hashes = [kzg_commitment_to_versioned_hash(commitment) for commitment in envelope.blob_kzg_commitments] + 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, + ) + ) + # Cache the execution payload header and proposer + state.latest_block_hash = payload.block_hash + state.latest_full_slot = state.slot + state.previous_inclusion_list_proposer = state.latest_inclusion_list_proposer + state.previous_inclusion_list_slot = state.latest_inclusion_list_slot + # Verify the state root + if verify: + assert envelope.state_root == hash_tree_root(state) +``` diff --git a/specs/_features/epbs/builder.md b/specs/_features/epbs/builder.md new file mode 100644 index 0000000000..75bd69200c --- /dev/null +++ b/specs/_features/epbs/builder.md @@ -0,0 +1,116 @@ +# ePBS -- Honest Builder + +This is an accompanying document which describes the expected actions of a "builder" participating in the Ethereum proof-of-stake protocol. + + + + +- [Introduction](#introduction) +- [Builders attributions](#builders-attributions) + - [Constructing the payload bid](#constructing-the-payload-bid) + - [Constructing the `BlobSidecar`s](#constructing-the-blobsidecars) + - [Constructing the execution payload envelope](#constructing-the-execution-payload-envelope) + - [Honest payload withheld messages](#honest-payload-withheld-messages) + + + +## Introduction + +With the ePBS Fork, the protocol includes new staked participants of the protocol called *Builders*. While Builders are a subset of the validator set, they have extra attributions that are optional. Validators may opt to not be builders and as such we collect the set of guidelines for those validators that want to act as builders in this document. + +## Builders attributions + +Builders can submit bids to produce execution payloads. They can broadcast these bids in the form of `SignedExecutionPayloadHeader` objects, these objects encode a commitment to reveal an execution payload in exchange for a payment. When their bids are chosen by the corresponding proposer, builders are expected to broadcast an accompanying `SignedExecutionPayloadEnvelope` object honoring the commitment. + +Thus, builders tasks are divided in two, submitting bids, and submitting payloads. + +### Constructing the payload bid + +Builders can broadcast a payload bid for the current or the next slot's proposer to include. They produce a `SignedExecutionPayloadHeader` as follows. + +Prior to constructing a payload, the builder **MUST** have a full `InclusionList` object from the proposer matching `state.previous_inclusion_list_proposer`. The signed summary for this inclusion list will be needed when revealing the full execution payload (see below). +1. Set `header.parent_block_hash` to the current head of the execution chain (this can be obtained from the beacon state as `state.last_block_hash`). +2. Set `header.parent_block_root` to be the head of the consensus chain (this can be obtained from the beacon state as `hash_tree_root(state.latest_block_header)`. +3. Construct an execution payload. This can be performed with an external execution engine with a call to `engine_getPayloadV4`. +4. Set `header.block_hash` to be the block hash of the constructed payload, that is `payload.block_hash` +5. Set `header.builder_index` to be the validator index of the builder performing these actions. +6. Set `header.slot` to be the slot for which this bid is aimed. This slot **MUST** be either the current slot or the next slot. +7. Set `header.value` to be the value that the builder will pay the proposer if the bid is accepted. The builder **MUST** have balance enough to fulfill this bid. +8. Set `header.kzg_commitments_root` to be the `hash_tree_root` of the `blobsbundle.commitments` field returned by `engine_getPayloadV4`. + +After building the `header`, the builder obtains a `signature` of the header by using + +```python +def get_execution_payload_header_signature(state: BeaconState, header: ExecutionPayloadHeader, privkey: int) -> BLSSignature: + domain = get_domain(state, DOMAIN_BEACON_BUILDER, compute_epoch_at_slot(header.slot)) + signing_root = compute_signing_root(header, domain) + return bls.Sign(privkey, signing_root) +``` + +The builder assembles then `signed_exceution_payload_header = SignedExecutionPayloadHeader(message=header, signature=signature)` and broadcasts it on the `execution_payload_header` global gossip topic. + +### Constructing the `BlobSidecar`s + +[Modified in ePBS] + +The `BlobSidecar` container is modified indirectly because the constant `KZG_COMMITMENT_INCLUSION_PROOF_DEPTH` is modified. Each sidecar is obtained from the modified + +```python +def get_blob_sidecars(signed_block: SignedBeaconBlock, + blobs: Sequence[Blob], + blob_kzg_proofs: Sequence[KZGProof]) -> Sequence[BlobSidecar]: + block = signed_block.message + block_header = BeaconBlockHeader( + slot=block.slot, + proposer_index=block.proposer_index, + parent_root=block.parent_root, + state_root=block.state_root, + body_root=hash_tree_root(block.body), + ) + signed_block_header = SignedBeaconBlockHeader(message=block_header, signature=signed_block.signature) + return [ + BlobSidecar( + index=index, + blob=blob, + kzg_commitment=block.body.blob_kzg_commitments[index], + kzg_proof=blob_kzg_proofs[index], + signed_block_header=signed_block_header, + kzg_commitment_inclusion_proof=compute_merkle_proof( + block.body, + GeneralizedIndex(KZG_GENERALIZED_INDEX_PREFIX + index), + ), + ) + for index, blob in enumerate(blobs) + ] +``` + +### Constructing the execution payload envelope + +When the proposer publishes a valid `SignedBeaconBlock` containing a signed commitment by the builder, the builder is later expected to broadcast the corresponding `SignedExecutionPayloadEnvelope` that fulfills this commitment. See below for a special case of an *honestly withheld payload*. + +To construct the `execution_payload_envelope` the builder must perform the following steps, we alias `header` to be the committed `ExecutionPayloadHeader` in the beacon block. + +1. Set the `payload` field to be the `ExecutionPayload` constructed when creating the corresponding bid. This payload **MUST** have the same block hash as `header.block_hash`. +2. Set the `builder_index` field to be the validator index of the builder performing these steps. This field **MUST** be `header.builder_index`. +3. Set `beacon_block_root` to be the `hash_tree_root` of the corresponding beacon block. +4. Set `blob_kzg_commitments` to be the `commitments` field of the blobs bundle constructed when constructing the bid. This field **MUST** have a `hash_tree_root` equal to `header.blob_kzg_commitments_root`. +5. Set `inclusion_list_proposer_index` to be the `inclusion_list_summary.proposer_index` from the inclusion list used when creating the bid. +6. Set `inclusion_list_slot` to be the `inclusion_list_summary.slot` from the inclusion list used when creating the bid. +7. Set the `inclusion_list_signature` to be `signed_inclusion_list_summary.signature` from the inclusion list used when creating the bid. +8. Set `payload_witheld` to `False`. + +After setting these parameters, the builder should run `process_execution_payload(state, signed_envelope, verify=False)` and this function should not trigger an exception. + +9. Set `state_root` to `hash_tree_root(state)`. +After preparing the `envelope` the builder should sign the envelope using: +```python +def get_execution_payload_envelope_signature(state: BeaconState, envelope: ExecutionPayloadEnvelope, privkey: int) -> BLSSignature: + domain = get_domain(state, DOMAIN_BEACON_BUILDER, compute_epoch_at_slot(state.slot)) + signing_root = compute_signing_root(envelope, domain) + return bls.Sign(privkey, signing_root) +``` +The builder assembles then `signed_exceution_payload_envelope = SignedExecutionPayloadEnvelope(message=envelope, signature=signature)` and broadcasts it on the `execution_payload` global gossip topic. + +### Honest payload withheld messages + +An honest builder that has seen a `SignedBeaconBlock` referencing his signed bid, but that block was not timely and thus it is not the head of the builder's chain, may choose to withhold their execution payload. For this the builder should simply act as if it were building an empty payload, without any transactions, withdrawals, etc. Omit step 9 above and set `payload_withheld` to `True`. If the PTC sees this message and votes for it, validators will attribute a *withholding boost* to the builder, which would increase the forkchoice weight of the parent block, favouring it and preventing the builder from being charged for the bid by not revealing. diff --git a/specs/_features/epbs/engine.md b/specs/_features/epbs/engine.md new file mode 100644 index 0000000000..5989dde0ff --- /dev/null +++ b/specs/_features/epbs/engine.md @@ -0,0 +1,220 @@ +# Engine API -- ePBS + +Engine API changes introduced in the ePBS fork. + +## Table of contents + + + + +- [Structures](#structures) + - [ExecutionPayloadV4](#executionpayloadv4) + - [PayloadAttributesV3](#payloadattributesv3) + - [InclusionListV1](#inclusionlistv1) + - [InclusionListStatusV1](#inclusionliststatusv1) +- [Methods](#methods) + - [engine_newInclusionListV1](#engine_newinclusionlistv1) + - [Request](#request) + - [Response](#response) + - [Specification](#specification) + - [engine_newPayloadV4](#engine_newpayloadv4) + - [Request](#request-1) + - [Response](#response-1) + - [Specification](#specification-1) + - [engine_forkchoiceUpdatedV3](#engine_forkchoiceupdatedv3) + - [Request](#request-2) + - [Response](#response-2) + - [Specification](#specification-2) + - [engine_getPayloadV3](#engine_getpayloadv3) + - [Request](#request-3) + - [Response](#response-3) + - [Specification](#specification-3) + - [`engine_getInclusionListV1`](#engine_getinclusionlistv1) + - [Request](#request-4) + - [Response](#response-4) + + + +## Structures + +### ExecutionPayloadV4 + +This structure has the syntax of [`ExecutionPayloadV3`](./cancun.md#executionpayloadv3) and appends the new field `inclusionListSummary` + +- `parentHash`: `DATA`, 32 Bytes +- `feeRecipient`: `DATA`, 20 Bytes +- `stateRoot`: `DATA`, 32 Bytes +- `receiptsRoot`: `DATA`, 32 Bytes +- `logsBloom`: `DATA`, 256 Bytes +- `prevRandao`: `DATA`, 32 Bytes +- `blockNumber`: `QUANTITY`, 64 Bits +- `gasLimit`: `QUANTITY`, 64 Bits +- `gasUsed`: `QUANTITY`, 64 Bits +- `timestamp`: `QUANTITY`, 64 Bits +- `extraData`: `DATA`, 0 to 32 Bytes +- `baseFeePerGas`: `QUANTITY`, 256 Bits +- `blockHash`: `DATA`, 32 Bytes +- `transactions`: `Array of DATA` - Array of transaction objects, each object is a byte list (`DATA`) representing `TransactionType || TransactionPayload` or `LegacyTransaction` as defined in [EIP-2718](https://eips.ethereum.org/EIPS/eip-2718) +- `withdrawals`: `Array of WithdrawalV1` - Array of withdrawals, each object is an `OBJECT` containing the fields of a `WithdrawalV1` structure. +- `blobGasUsed`: `QUANTITY`, 64 Bits +- `excessBlobGas`: `QUANTITY`, 64 Bits +- `inclusionListSummary`: `Array of (DATA, 20 Bytes)` - The summary of an inclusion list signed by a previous proposer. + +### PayloadAttributesV3 + +This structure has the syntax of [`PayloadAttributesV2`](./shanghai.md#payloadattributesv2) and appends the fields `inclusionListParentHash` and `inclusionListProposerIndex`. + +- `timestamp`: `QUANTITY`, 64 Bits - value for the `timestamp` field of the new payload +- `prevRandao`: `DATA`, 32 Bytes - value for the `prevRandao` field of the new payload +- `suggestedFeeRecipient`: `DATA`, 20 Bytes - suggested value for the `feeRecipient` field of the new payload +- `withdrawals`: `Array of WithdrawalV1` - Array of withdrawals, each object is an `OBJECT` containing the fields of a `WithdrawalV1` structure. +- `parentBeaconBlockRoot`: `DATA`, 32 Bytes - Root of the parent beacon block. +- `inclusionListParentHash`: `DATA`, 32 Bytes - Hash of the parent block of the required inclusion list. +- `inclusionListProposer`: 64 Bits - Validator index of the proposer of the inclusion list. + +### InclusionListV1 + +[New in ePBS] + +This structure contains the full list of transactions and the summary broadcast by a proposer of an inclusion list. + +- `transactions`: `Array of DATA` - Array of transaction objects, each object is a byte list (`DATA`) representing `TransactionType || TransactionPayload` or `LegacyTransaction` as defined in [EIP-2718](https://eips.ethereum.org/EIPS/eip-2718) +- `summary`: `Array of DATA` - Array of addresses, each object is a byte list (`DATA`, 20 Bytes) representing the "from" address from the transactions in the `transactions` list. +- `parentHash`: `DATA`, 32 Bytes. +- `proposerIndex` : `QUANTITY`, 64 Bits. + +### InclusionListStatusV1 + +[New in ePBS] + +This structure contains the result of processing an inclusion list. The fields are encoded as follows: + +- `status`: `enum`- `"VALID" | "INVALID" | "SYNCING" | "ACCEPTED"` +- `validationError`: `String|null` - a message providing additional details on the validation error if the inclusion list is classified as `INVALID`. + +## Methods + +### engine_newInclusionListV1 + +[New in ePBS] + +#### Request + +* method: `engine_newInclusionListV1` +* params: + 1. `inclusion_list`: [`InclusionListV1`](#InclusionListV1). + +#### Response + +- result: [`InclusionListStatusV1`](#InclusionListStatusV1). +- error: code and message set in case an exception happens while processing the payload. + +#### Specification + +### engine_newPayloadV4 + +#### Request + +* method: `engine_newPayloadV3` +* params: + 1. `executionPayload`: [`ExecutionPayloadV3`](#ExecutionPayloadV3). + 2. `expectedBlobVersionedHashes`: `Array of DATA`, 32 Bytes - Array of expected blob versioned hashes to validate. + 3. `parentBeaconBlockRoot`: `DATA`, 32 Bytes - Root of the parent beacon block. + +#### Response + +Refer to the response for [`engine_newPayloadV2`](./shanghai.md#engine_newpayloadv2). + +#### Specification + +This method follows the same specification as [`engine_newPayloadV2`](./shanghai.md#engine_newpayloadv2) with the addition of the following: + +1. Client software **MUST** check that provided set of parameters and their fields strictly matches the expected one and return `-32602: Invalid params` error if this check fails. Any field having `null` value **MUST** be considered as not provided. + +2. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of the payload does not fall within the time frame of the Cancun fork. + +3. Given the expected array of blob versioned hashes client software **MUST** run its validation by taking the following steps: + 1. Obtain the actual array by concatenating blob versioned hashes lists (`tx.blob_versioned_hashes`) of each [blob transaction](https://eips.ethereum.org/EIPS/eip-4844#new-transaction-type) included in the payload, respecting the order of inclusion. If the payload has no blob transactions the expected array **MUST** be `[]`. + 2. Return `{status: INVALID, latestValidHash: null, validationError: errorMessage | null}` if the expected and the actual arrays don't match. + + This validation **MUST** be instantly run in all cases even during active sync process. + +### engine_forkchoiceUpdatedV3 + +#### Request + +* method: `engine_forkchoiceUpdatedV3` +* params: + 1. `forkchoiceState`: [`ForkchoiceStateV1`](./paris.md#ForkchoiceStateV1). + 2. `payloadAttributes`: `Object|null` - Instance of [`PayloadAttributesV3`](#payloadattributesv3) or `null`. +* timeout: 8s + +#### Response + +Refer to the response for [`engine_forkchoiceUpdatedV2`](./shanghai.md#engine_forkchoiceupdatedv2). + +#### Specification + +This method follows the same specification as [`engine_forkchoiceUpdatedV2`](./shanghai.md#engine_forkchoiceupdatedv2) with the following changes to the processing flow: + +1. Client software **MUST** verify that `forkchoiceState` matches the [`ForkchoiceStateV1`](./paris.md#ForkchoiceStateV1) structure and return `-32602: Invalid params` on failure. + +2. Extend point (7) of the `engine_forkchoiceUpdatedV1` [specification](./paris.md#specification-1) by defining the following sequence of checks that **MUST** be run over `payloadAttributes`: + + 1. `payloadAttributes` matches the [`PayloadAttributesV3`](#payloadattributesv3) structure, return `-38003: Invalid payload attributes` on failure. + + 2. `payloadAttributes.timestamp` falls within the time frame of the Cancun fork, return `-38005: Unsupported fork` on failure. + + 3. `payloadAttributes.timestamp` is greater than `timestamp` of a block referenced by `forkchoiceState.headBlockHash`, return `-38003: Invalid payload attributes` on failure. + + 4. If any of the above checks fails, the `forkchoiceState` update **MUST NOT** be rolled back. + +### engine_getPayloadV3 + +The response of this method is extended with [`BlobsBundleV1`](#blobsbundlev1) containing the blobs, their respective KZG commitments +and proofs corresponding to the `versioned_hashes` included in the blob transactions of the execution payload. + +#### Request + +* method: `engine_getPayloadV3` +* params: + 1. `payloadId`: `DATA`, 8 Bytes - Identifier of the payload build process +* timeout: 1s + +#### Response + +* result: `object` + - `executionPayload`: [`ExecutionPayloadV3`](#ExecutionPayloadV3) + - `blockValue` : `QUANTITY`, 256 Bits - The expected value to be received by the `feeRecipient` in wei + - `blobsBundle`: [`BlobsBundleV1`](#BlobsBundleV1) - Bundle with data corresponding to blob transactions included into `executionPayload` + - `shouldOverrideBuilder` : `BOOLEAN` - Suggestion from the execution layer to use this `executionPayload` instead of an externally provided one +* error: code and message set in case an exception happens while getting the payload. + +#### Specification + +Refer to the specification for [`engine_getPayloadV2`](./shanghai.md#engine_getpayloadv2) with addition of the following: + +1. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of the built payload does not fall within the time frame of the Cancun fork. + +2. The call **MUST** return `blobsBundle` with empty `blobs`, `commitments` and `proofs` if the payload doesn't contain any blob transactions. + +3. The call **MUST** return `commitments` matching the versioned hashes of the transactions list of the execution payload, in the same order, + i.e. `assert verify_kzg_commitments_against_transactions(payload.transactions, blobsBundle.commitments)` (see EIP-4844 consensus-specs). + +4. The call **MUST** return `blobs` and `proofs` that match the `commitments` list, i.e. `assert len(blobsBundle.commitments) == len(blobsBundle.blobs) == len(blobsBundle.proofs)` and `assert verify_blob_kzg_proof_batch(blobsBundle.blobs, blobsBundle.commitments, blobsBundle.proofs)`. + +5. Client software **MAY** use any heuristics to decide whether to set `shouldOverrideBuilder` flag or not. If client software does not implement any heuristic this flag **SHOULD** be set to `false`. + +### `engine_getInclusionListV1` + +#### Request + +* method: `engine_getInclusionListV1` +* params: + 1. `parentHash`: `DATA`, 32 Bytes - hash of the block which the returning inclusion list bases on +* timeout: 1s + +#### Response + +* result: [`InclusionListV1`](#inclusionlistv1) +* error: code and message set in case an exception happens while getting the inclusion list. \ No newline at end of file diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md new file mode 100644 index 0000000000..35f5e510af --- /dev/null +++ b/specs/_features/epbs/fork-choice.md @@ -0,0 +1,634 @@ +# ePBS -- Fork Choice + +## Table of contents + + + + +- [Introduction](#introduction) +- [Constant](#constant) +- [Containers](#containers) + - [New `ChildNode`](#new-childnode) +- [Helpers](#helpers) + - [Modified `LatestMessage`](#modified-latestmessage) + - [Modified `update_latest_messages`](#modified-update_latest_messages) + - [Modified `Store`](#modified-store) + - [`verify_inclusion_list`](#verify_inclusion_list) + - [`blocks_for_slot`](#blocks_for_slot) + - [`block_for_inclusion_list`](#block_for_inclusion_list) + - [`on_inclusion_list`](#on_inclusion_list) + - [`notify_ptc_messages`](#notify_ptc_messages) + - [`is_payload_present`](#is_payload_present) + - [`is_parent_node_full`](#is_parent_node_full) + - [Modified `get_ancestor`](#modified-get_ancestor) + - [Modified `get_checkpoint_block`](#modified-get_checkpoint_block) + - [`is_supporting_vote`](#is_supporting_vote) + - [New `compute_proposer_boost`](#new-compute_proposer_boost) + - [New `compute_withhold_boost` `](#new-compute_withhold_boost-) + - [New `compute_reveal_boost` `](#new-compute_reveal_boost-) + - [Modified `get_weight`](#modified-get_weight) + - [New `get_head_no_il`](#new-get_head_no_il) + - [Modified `get_head`](#modified-get_head) +- [Engine APIs](#engine-apis) + - [New `NewInclusionListRequest`](#new-newinclusionlistrequest) + - [New `notify_new_inclusion_list`](#new-notify_new_inclusion_list) +- [Updated fork-choice handlers](#updated-fork-choice-handlers) + - [`on_block`](#on_block) +- [New fork-choice handlers](#new-fork-choice-handlers) + - [`on_execution_payload`](#on_execution_payload) + - [`seconds_into_slot`](#seconds_into_slot) + - [Modified `on_tick_per_slot`](#modified-on_tick_per_slot) + - [`on_payload_attestation_message`](#on_payload_attestation_message) + + + + +## Introduction + +This is the modification of the fork choice accompanying the ePBS upgrade. + +## Constant + +| Name | Value | +| -------------------- | ----------- | +| `PAYLOAD_TIMELY_THRESHOLD` | `PTC_SIZE/2` (=`uint64(256)`) | +| `PROPOSER_SCORE_BOOST` | `20` (modified in ePBS]) | +| `PAYLOAD_WITHHOLD_BOOST` | `40` | +| `PAYLOAD_REVEAL_BOOST` | `40` | + +## Containers + +### New `ChildNode` +Auxiliary class to consider `(block, slot, bool)` LMD voting + +```python +class ChildNode(Container): + root: Root + slot: Slot + is_payload_present: bool +``` + +## Helpers + +### Modified `LatestMessage` +**Note:** The class is modified to keep track of the slot instead of the epoch. + +```python +@dataclass(eq=True, frozen=True) +class LatestMessage(object): + slot: Slot + root: Root +``` + +### Modified `update_latest_messages` +**Note:** the function `update_latest_messages` is updated to use the attestation slot instead of target. Notice that this function is only called on validated attestations and validators cannot attest twice in the same epoch without equivocating. Notice also that target epoch number and slot number are validated on `validate_on_attestation`. + +```python +def update_latest_messages(store: Store, attesting_indices: Sequence[ValidatorIndex], attestation: Attestation) -> None: + slot = attestation.data.slot + beacon_block_root = attestation.data.beacon_block_root + non_equivocating_attesting_indices = [i for i in attesting_indices if i not in store.equivocating_indices] + for i in non_equivocating_attesting_indices: + if i not in store.latest_messages or slot > store.latest_messages[i].slot: + store.latest_messages[i] = LatestMessage(slot=slot, root=beacon_block_root) +``` + +### Modified `Store` +**Note:** `Store` is modified to track the intermediate states of "empty" consensus blocks, that is, those consensus blocks for which the corresponding execution payload has not been revealed or has not been included on chain. + +```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) + 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) + execution_payload_states: Dict[Root, BeaconState] = field(default_factory=dict) # [New in ePBS] + inclusion_list_available: Dict[Root, bool] = field(default_factory=dict) # [New in ePBS] + ptc_vote: Dict[Root, Vector[uint8, PTC_SIZE]] = field(default_factory=dict) # [New in ePBS] + payload_withhold_boost_root: Root # [New in ePBS] + payload_withhold_boost_full: bool # [New in ePBS] + payload_reveal_boost_root: Root # [New in ePBS] +``` + +### `verify_inclusion_list` +*[New in ePBS]* + +```python +def verify_inclusion_list(state: BeaconState, block: BeaconBlock, inclusion_list: InclusionList, execution_engine: ExecutionEngine) -> bool: + """ + Returns true if the inclusion list is valid. + """ + # Check that the inclusion list corresponds to the block proposer + signed_summary = inclusion_list.summary + proposer_index = signed_summary.message.proposer_index + assert block.proposer_index == proposer_index + + # Check that the signature is correct + assert verify_inclusion_list_summary_signature(state, signed_summary) + + # TODO: These checks will also be performed by the EL surely so we can probably remove them from here. + # Check the summary and transaction list lengths + summary = signed_summary.message.summary + assert len(summary) <= MAX_TRANSACTIONS_PER_INCLUSION_LIST + assert len(inclusion_list.transactions) == len(summary) + + # Check that the inclusion list is valid + return execution_engine.notify_new_inclusion_list(NewInclusionListRequest( + inclusion_list=inclusion_list.transactions, + summary=summary, + parent_block_hash = state.latest_block_hash)) +``` + +### `blocks_for_slot` +*[New in ePBS]* + +The function `blocks_for_slot` returns all the beacon blocks found in the store for the given slot. This implementation here is only for specification purposes and clients may choose to optimize this by using an internal map or similar caching structures. + +```python +def blocks_for_slot(store: Store, slot: Slot) -> Set[BeaconBlock]: + return [block for root, block in store.blocks.items() if block.slot == slot] +``` + +### `block_for_inclusion_list` +*[New in ePBS]* +The function `block_for_inclusion_list` returns a known beacon block in store that is compatible with the given inclusion list. + +```python +def block_for_inclusion_list(store: Store, inclusion_list: InclusionList) -> Optional[BeaconBlock]: + summary = inclusion_list.signed_summary.message + parent_hash = inclusion_list.parent_block_hash + + blocks = blocks_for_slot(store, summary.slot) + for block in blocks: + if block.proposer_index == summary.proposer_index and block.signed_execution_payload_header.message.parent_block_hash == parent_hash: + return block + return None +``` + +### `on_inclusion_list` +*[New in ePBS]* + +The function `on_inclusion_list` is called every time an `InclusionList` is seen by the node that passes pubsub validation. This specification requires that there is already a beacon block in the store that is compatible with this inclusion list. Client developers may (and should) instead validate the inclusion list against the head state in case it arrives earlier than the beacon block and cache this result. + +```python +def on_inclusion_list(store: Store, inclusion_list: InclusionList) -> None: + """ + Validates an incoming inclusion list and records the result in the corresponding forkchoice node. + """ + # Require we have one block compatible with the inclusion list + block = block_for_inclusion_list(store, inclusion_list) + assert block is not None + root = hash_tree_root(block) + assert root in store.block_states + state = store.block_states[root] + assert block.parent_root in store.blocks + parent_block = store.blocks[block.parent_root] + + # Ignore the list if the parent consensus block did not contain a payload + header = block.body.signed_execution_payload_header.message + parent_header = parent_block.body.signed_execution_payload_header.message + if header.parent_block_hash != parent_header.block_hash: + assert header.parent_block_hash == parent_header.parent_block_hash + return + + # verify the inclusion list + assert verify_inclusion_list(state, block, inclusion_list, EXECUTION_ENGINE) + store.inclusion_list_available[root]=True +``` + +### `notify_ptc_messages` + +```python +def notify_ptc_messages(store: Store, state: BeaconState, payload_attestations: Sequence[PayloadAttestation]) -> None: + """ + Extracts a list of ``PayloadAttestationMessage`` from ``payload_attestations`` and updates the store with them + These Payload attestations are assumed to be in the beacon block hence signature verification is not needed + """ + if state.slot == 0: + return + for payload_attestation in payload_attestations: + indexed_payload_attestation = get_indexed_payload_attestation(state, state.slot - 1, payload_attestation) + for idx in indexed_payload_attestation.attesting_indices: + store.on_payload_attestation_message(PayloadAttestationMessage(validator_index=idx, + data=payload_attestation.data, signature= BLSSignature(), is_from_block=true)) +``` + +### `is_payload_present` + +```python +def is_payload_present(store: Store, beacon_block_root: Root) -> bool: + """ + Return whether the execution payload for the beacon block with root ``beacon_block_root`` was voted as present + by the PTC + """ + # The beacon block root must be known + assert beacon_block_root in store.ptc_vote + return store.ptc_vote[beacon_block_root].count(PAYLOAD_PRESENT) > PAYLOAD_TIMELY_THRESHOLD +``` + +### `is_parent_node_full` + +```python +def is_parent_node_full(store: Store, block: BeaconBlock) -> bool: + parent = store.blocks[block.parent_root] + return block.body.signed_execution_payload_header.message.parent_block_hash == parent.body.signed_execution_payload_header.message.block_hash +``` + +### Modified `get_ancestor` +**Note:** `get_ancestor` is modified to return whether the chain is based on an *empty* or *full* block. + +```python +def get_ancestor(store: Store, root: Root, slot: Slot) -> ChildNode: + """ + Returns the beacon block root, the slot and the payload status of the ancestor of the beacon block + with ``root`` at ``slot``. If the beacon block with ``root`` is already at ``slot`` or we are + requesting an ancestor "in the future" it returns its PTC status instead of the actual payload content. + """ + block = store.blocks[root] + if block.slot <= slot: + return ChildNode(root=root, slot=slot, is_payload_present=is_payload_present(store, root)) + + parent = store.blocks[block.parent_root] + if parent.slot > slot: + return get_ancestor(store, block.parent_root, slot) + return ChildNode(root=block.parent_root, slot=parent.slot, is_payload_present=is_parent_node_full(block)) +``` + +### Modified `get_checkpoint_block` +**Note:** `get_checkpoint_block` is modified to use the new `get_ancestor` + +```python +def get_checkpoint_block(store: Store, root: Root, epoch: Epoch) -> Root: + """ + Compute the checkpoint block for epoch ``epoch`` in the chain of block ``root`` + """ + epoch_first_slot = compute_start_slot_at_epoch(epoch) + return get_ancestor(store, root, epoch_first_slot).root +``` + + +### `is_supporting_vote` + +```python +def is_supporting_vote(store: Store, node: ChildNode, message: LatestMessage) -> bool: + """ + Returns whether a vote for ``message.root`` supports the chain containing the beacon block ``node.root`` with the + payload contents indicated by ``node.is_payload_present`` as head during slot ``node.slot``. + """ + if node.root == message.root: + # an attestation for a given root always counts for that root regardless if full or empty + # as long as the attestation happened after the requested slot. + return node.slot <= message.slot + message_block = store.blocks[message.root] + if node.slot >= message_block.slot: + return False + ancestor = get_ancestor(store, message.root, node.slot) + return (node.root == ancestor.root) and (node.is_payload_present == ancestor.is_payload_present) +``` + +### New `compute_proposer_boost` +This is a helper to compute the proposer boost. It applies the proposer boost to any ancestor of the proposer boost root taking into account the payload presence. There is one exception: if the requested node has the same root and slot as the block with the proposer boost root, then the proposer boost is applied to both empty and full versions of the node. +```python +def compute_proposer_boost(store: Store, state: State, node: ChildNode) -> Gwei: + if store.proposer_boost_root == Root(): + return Gwei(0) + ancestor = get_ancestor(store, store.proposer_boost_root, node.slot) + if ancestor.root != node.root: + return Gwei(0) + proposer_boost_slot = store.blocks[store.proposer_boost_root].slot + # Proposer boost is not applied after skipped slots + if node.slot > proposer_boost_slot: + return Gwei(0) + if (node.slot < proposer_boost_slot) and (ancestor.is_payload_present != node.is_payload_present): + return Gwei(0) + committee_weight = get_total_active_balance(state) // SLOTS_PER_EPOCH + return (committee_weight * PROPOSER_SCORE_BOOST) // 100 +``` + +### New `compute_withhold_boost` ` +This is a similar helper that applies for the withhold boost. In this case this always takes into account the reveal status. + +```python +def compute_withhold_boost(store: Store, state: State, node: ChildNode) -> Gwei: + if store.payload_withhold_boost_root == Root(): + return Gwei(0) + ancestor = get_ancestor(store, store.payload_withold_boost_root, node.slot) + if ancestor.root != node.root: + return Gwei(0) + if node.slot >= store.blocks[store.payload_withhold_boost_root].slot: + ancestor.is_payload_present = store.payload_withhold_boost_full + if ancestor.is_payload_present != node.is_payload_present: + return Gwei(0) + + committee_weight = get_total_active_balance(state) // SLOTS_PER_EPOCH + return (committee_weight * PAYLOAD_WITHHOLD_BOOST) // 100 +``` + +### New `compute_reveal_boost` ` +This is a similar helper to the last two, the only difference is that the reveal boost is only applied to the full version of the node when querying for the same slot as the revealed payload. + +```python +def compute_reveal_boost(store: Store, state: State, node: ChildNode) -> Gwei: + if store.payload_reveal_boost_root == Root(): + return Gwei(0) + ancestor = get_ancestor(store, store.payload_reveal_boost_root, node.slot) + if ancestor.root != node.root: + return Gwei(0) + if node.slot >= store.blocks[store.payload_reveal_boost_root].slot: + ancestor.is_payload_present = True + if is_ancestor_full != node.is_payload_present: + return Gwei(0) + committee_weight = get_total_active_balance(state) // SLOTS_PER_EPOCH + return (committee_weight * PAYLOAD_REVEAL_BOOST) // 100 +``` + +### Modified `get_weight` + +**Note:** `get_weight` is modified to only count votes for descending chains that support the status of a triple `Root, Slot, bool`, where the `bool` indicates if the block was full or not. `Slot` is needed for a correct implementation of `(Block, Slot)` voting. + +```python +def get_weight(store: Store, node: ChildNode) -> Gwei: + state = store.checkpoint_states[store.justified_checkpoint] + unslashed_and_active_indices = [ + i for i in get_active_validator_indices(state, get_current_epoch(state)) + if not is_slashed_attester(state.validators[i]) + ] + attestation_score = Gwei(sum( + state.validators[i].effective_balance for i in unslashed_and_active_indices + if (i in store.latest_messages + and i not in store.equivocating_indices + and is_supporting_vote(store, node, store.latest_messages[i])) + )) + + # Compute boosts + proposer_score = compute_boost(store, state, node) + builder_reveal_score = compute_reveal_boost(store, state, node) + builder_withhold_score = compute_withhold_boost(store, state, node) + + return attestation_score + proposer_score + builder_reveal_score + builder_withhold_score +``` + +### New `get_head_no_il` + +**Note:** `get_head_no_il` is a modified version of `get_head` to use the new `get_weight` function. It returns the Beacon block root of the head block and whether its payload is considered present or not. It disregards IL availability. + +```python +def get_head_no_il(store: Store) -> ChildNode: + # Get filtered block tree that only includes viable branches + blocks = get_filtered_block_tree(store) + # Execute the LMD-GHOST fork choice + justified_root = store.justified_checkpoint.root + justified_block = store.blocks[justified_root] + justified_slot = justified_block.slot + justified_full = is_payload_present(store, justified_root) + best_child = ChildNode(root=head_root, slot=head_slot, is_payload_present=head_full) + while True: + children = [ + ChildNode(root=root, slot=block.slot, is_payload_present=present) for (root, block) in blocks.items() + if block.parent_root == best_child.root and + is_parent_node_full(store, block) == best_child.is_payload_present if root != store.justified_root + for present in (True, False) if root in store.execution_payload_states or present == False + ] + if len(children) == 0: + return best_child + # if we have children we consider the current head advanced as a possible head + children += [ChildNode(root=best_child.root, slot=best_child.slot + 1, best_child.is_payload_present)] + # Sort by latest attesting balance with ties broken lexicographically + # Ties broken by favoring full blocks according to the PTC vote + # Ties are then broken by favoring full blocks + # Ties broken then by favoring higher slot numbers + # Ties then broken by favoring block with lexicographically higher root + new_best_child = max(children, key=lambda child: (get_weight(store, child), is_payload_present(store, child.root), child.is_payload_present, child.slot, child.root)) + if new_best_child.root == best_child.root: + return new_best_child + best_child = new_best_child +``` + +### Modified `get_head` +`get_head` is modified to use the new weight system by `(block, slot, payload_present)` voting and to not consider nodes without an available inclusion list. + +```python +def get_head(store: Store) -> ChildNode: + head = get_head_no_il(store) + while not store.inclusion_list_available(head.root): + head = get_ancestor(store, head.root, head.slot - 1) + return head +``` + +## Engine APIs + +### New `NewInclusionListRequest` + +```python +@dataclass +class NewInclusionListRequest(object): + inclusion_list: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] + summary: List[ExecutionAddress, MAX_TRANSACTIONS_PER_INCLUSION_LIST] + parent_block_hash: Hash32 +``` + + +### New `notify_new_inclusion_list` + +```python +def notify_new_inclusion_list(self: ExecutionEngine, + inclusion_list_request: NewInclusionListRequest) -> bool: + """ + Return ``True`` if and only if the transactions in the inclusion list can be succesfully executed + starting from the execution state corresponding to the `parent_block_hash` in the inclusion list + summary. The execution engine also checks that the total gas limit is less or equal that + ```MAX_GAS_PER_INCLUSION_LIST``, and the transactions in the list of transactions correspond to the signed summary + """ + ... +``` + +## Updated fork-choice handlers + +### `on_block` + +*Note*: The handler `on_block` is modified to consider the pre `state` of the given consensus beacon block depending not only on the parent block root, but also on the parent blockhash. In addition we delay the checking of blob data availability until the processing of the execution payload. + +```python +def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: + """ + Run ``on_block`` upon receiving a new block. + """ + block = signed_block.message + # Parent block must be known + assert block.parent_root in store.block_states + + # Check if this blocks builds on empty or full parent block + parent_block = store.blocks[block.parent_root] + header = block.body.signed_execution_payload_header.message + parent_header = parent_block.body.signed_execution_payload_header.message + # Make a copy of the state to avoid mutability issues + if is_parent_node_full(store, block): + assert block.parent_root in store.execution_payload_states + state = copy(store.execution_payload_states[block.parent_root]) + else: + assert header.parent_block_hash == parent_header.parent_block_hash + state = copy(store.block_states[block.parent_root]) + + # Blocks cannot be in the future. If they are, their consideration must be delayed until they are in the past. + current_slot = get_current_slot(store) + assert current_slot >= block.slot + + # Check that block is later than the finalized epoch slot (optimization to reduce calls to get_ancestor) + finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch) + assert block.slot > finalized_slot + # Check block is a descendant of the finalized block at the checkpoint finalized slot + finalized_checkpoint_block = get_checkpoint_block( + store, + block.parent_root, + store.finalized_checkpoint.epoch, + ) + assert store.finalized_checkpoint.root == finalized_checkpoint_block + + # Check the block is valid and compute the post-state + block_root = hash_tree_root(block) + state_transition(state, signed_block, True) + + # Add new block to the store + store.blocks[block_root] = block + # Add new state for this block to the store + store.block_states[block_root] = state + # Add a new PTC voting for this block to the store + store.ptc_vote[block_root] = [PAYLOAD_ABSENT]*PTC_SIZE + # if the parent block is empty record that the inclusion list for this block has been satisfied + if not is_parent_node_full(store, block): + store.inclusion_list_available = True + + # Notify the store about the payload_attestations in the block + store.notify_ptc_messages(state, block.body.payload_attestations) + # Add proposer score boost if the block is timely + 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 get_current_slot(store) == block.slot and is_before_attesting_interval: + store.proposer_boost_root = hash_tree_root(block) + + # Update checkpoints in store if necessary + update_checkpoints(store, state.current_justified_checkpoint, state.finalized_checkpoint) + + # Eagerly compute unrealized justification and finality. + compute_pulled_up_tip(store, block_root) +``` + +## New fork-choice handlers + +### `on_execution_payload` + +```python +def on_execution_payload(store: Store, signed_envelope: SignedExecutionPayloadEnvelope) -> None: + """ + Run ``on_execution_payload`` upon receiving a new execution payload. + """ + envelope = signed_envelope.message + # The corresponding beacon block root needs to be known + assert envelope.beacon_block_root in store.block_states + + # Check if blob data is available + # If not, this payload MAY be queued and subsequently considered when blob data becomes available + assert is_data_available(envelope.beacon_block_root, envelope.blob_kzg_commitments) + + # Make a copy of the state to avoid mutability issues + state = copy(store.block_states[envelope.beacon_block_root]) + + # Process the execution payload + process_execution_payload(state, signed_envelope, EXECUTION_ENGINE) + + # Add new state for this payload to the store + store.execution_payload_states[envelope.beacon_block_root] = state +``` + +### `seconds_into_slot` + +```python +def seconds_into_slot(store: Store) -> uint64: + return (store.time - store.genesis_time) % SECONDS_PER_SLOT +``` + +### Modified `on_tick_per_slot` + +Modified to reset the payload boost roots + +```python +def on_tick_per_slot(store: Store, time: uint64) -> None: + previous_slot = get_current_slot(store) + + # Update store time + store.time = time + + current_slot = get_current_slot(store) + + # If this is a new slot, reset store.proposer_boost_root + if current_slot > previous_slot: + store.proposer_boost_root = Root() + else: + # Reset the payload boost if this is the attestation time + if seconds_into_slot(store) >= SECONDS_PER_SLOT // INTERVALS_PER_SLOT: + store.payload_withhold_boost_root = Root() + store.payload_withhold_boost_full = False + store.payload_reveal_boost_root = Root() + + # If a new epoch, pull-up justification and finalization from previous epoch + if current_slot > previous_slot and compute_slots_since_epoch_start(current_slot) == 0: + update_checkpoints(store, store.unrealized_justified_checkpoint, store.unrealized_finalized_checkpoint) +``` + +### `on_payload_attestation_message` + +```python +def on_payload_attestation_message(store: Store, + ptc_message: PayloadAttestationMessage, is_from_block: bool=False) -> None: + """ + Run ``on_payload_attestation_message`` upon receiving a new ``ptc_message`` directly on the wire. + """ + # The beacon block root must be known + data = ptc_message.data + # PTC attestation must be for a known block. If block is unknown, delay consideration until the block is found + state = store.block_states[data.beacon_block_root] + ptc = get_ptc(state, data.slot) + # PTC votes can only change the vote for their assigned beacon block, return early otherwise + if data.slot != state.slot: + return + # Check that the attester is from the PTC + assert ptc_message.validator_index in ptc + + # Verify the signature and check that its for the current slot if it is coming from the wire + if not is_from_block: + # Check that the attestation is for the current slot + assert data.slot == get_current_slot(store) + # Verify the signature + assert is_valid_indexed_payload_attestation(state, + IndexedPayloadAttestation(attesting_indices = [ptc_message.validator_index], data = data, + signature = ptc_message.signature)) + # Update the ptc vote for the block + ptc_index = ptc.index(ptc_message.validator_index) + ptc_vote = store.ptc_vote[data.beacon_block_root] + ptc_vote[ptc_index] = data.payload_status + + # Only update payload boosts with attestations from a block if the block is for the current slot and it's early + if is_from_block && data.slot + 1 != get_current_slot(store): + return + time_into_slot = (store.time - store.genesis_time) % SECONDS_PER_SLOT + if is_from_block and time_into_slot >= SECONDS_PER_SLOT // INTERVALS_PER_SLOT: + return + + # Update the payload boosts if threshold has been achieved + if ptc_vote.count(PAYLOAD_PRESENT) > PAYLOAD_TIMELY_THRESHOLD: + store.payload_reveal_boost_root = data.beacon_block_root + if ptc_vote.count(PAYLOAD_WITHHELD) > PAYLOAD_TIMELY_THRESHOLD: + block = store.blocks[data.beacon_block_root] + store.payload_withhold_boost_root = block.parent_root + store.payload_withhold_boost_full = is_parent_node_full(block) +``` diff --git a/specs/_features/epbs/fork.md b/specs/_features/epbs/fork.md new file mode 100644 index 0000000000..e665d7da8c --- /dev/null +++ b/specs/_features/epbs/fork.md @@ -0,0 +1,133 @@ +# ePBS -- Fork Logic + +**Notice**: This document is a work-in-progress for researchers and implementers. + +## Table of contents + + + + +- [Introduction](#introduction) +- [Configuration](#configuration) +- [Helper functions](#helper-functions) + - [Misc](#misc) + - [Modified `compute_fork_version`](#modified-compute_fork_version) +- [Fork to EPBS](#fork-to-epbs) + - [Fork trigger](#fork-trigger) + - [Upgrading the state](#upgrading-the-state) + + + +## Introduction + +This document describes the process of the ePBS upgrade. + +## Configuration + +Warning: this configuration is not definitive. + +| Name | Value | +|---------------------| - | +| `EPBS_FORK_VERSION` | `Version('0x05000000')` | +| `EPBS_FORK_EPOCH` | `Epoch(18446744073709551615)` **TBD** | + +## Helper functions + +### Misc + +#### Modified `compute_fork_version` + +```python +def compute_fork_version(epoch: Epoch) -> Version: + """ + Return the fork version at the given ``epoch``. + """ + if epoch >= EPBS_FORK_EPOCH: + return EPBS_FORK_VERSION + if epoch >= DENEB_FORK_EPOCH: + return DENEB_FORK_VERSION + if epoch >= CAPELLA_FORK_EPOCH: + return CAPELLA_FORK_VERSION + if epoch >= BELLATRIX_FORK_EPOCH: + return BELLATRIX_FORK_VERSION + if epoch >= ALTAIR_FORK_EPOCH: + return ALTAIR_FORK_VERSION + return GENESIS_FORK_VERSION +``` + +## Fork to EPBS + +### Fork trigger + +TBD. This fork is defined for testing purposes, the EIP may be combined with other +consensus-layer upgrade. +For now, we assume the condition will be triggered at epoch `EPBS_FORK_EPOCH`. + +### Upgrading the state + +If `state.slot % SLOTS_PER_EPOCH == 0` and `compute_epoch_at_slot(state.slot) == EPBS_FORK_EPOCH`, +an irregular state change is made to upgrade to ePBS. + +```python +def upgrade_to_epbs(pre: deneb.BeaconState) -> BeaconState: + epoch = deneb.get_current_epoch(pre) + + post = BeaconState( + # Versioning + genesis_time=pre.genesis_time, + genesis_validators_root=pre.genesis_validators_root, + slot=pre.slot, + fork=Fork( + previous_version=pre.fork.current_version, + current_version=EPBS_FORK_EPOCH, # [Modified in ePBS] + epoch=epoch, + ), + # History + latest_block_header=pre.latest_block_header, + block_roots=pre.block_roots, + state_roots=pre.state_roots, + historical_roots=pre.historical_roots, + # Eth1 + eth1_data=pre.eth1_data, + eth1_data_votes=pre.eth1_data_votes, + eth1_deposit_index=pre.eth1_deposit_index, + # Registry + validators=pre.validators, + balances=pre.balances, + # Randomness + randao_mixes=pre.randao_mixes, + # Slashings + slashings=pre.slashings, + # Participation + previous_epoch_participation=pre.previous_epoch_participation, + current_epoch_participation=pre.current_epoch_participation, + # Finality + justification_bits=pre.justification_bits, + previous_justified_checkpoint=pre.previous_justified_checkpoint, + current_justified_checkpoint=pre.current_justified_checkpoint, + finalized_checkpoint=pre.finalized_checkpoint, + # Inactivity + inactivity_scores=pre.inactivity_scores, + # Sync + current_sync_committee=pre.current_sync_committee, + next_sync_committee=pre.next_sync_committee, + # Execution-layer + # 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, + # ePBS + previous_inclusion_list_proposer=get_beacon_proposer_index(pre), # [New in ePBS] + previous_inclusion_list_slot=pre.slot, # [New in ePBS] + latest_inclusion_list_proposer=0, # [New in ePBS] + latest_inclusion_list_slot=0, # [New in ePBS] + latest_block_hash=Hash32(), # [New in ePBS] + latest_full_slot=pre.slot, # [New in ePBS] + execution_payload_header=ExecutionPayloadHeader(), # [New in ePBS] + last_withdrawals_root=Root(), # [New in ePBS] + ) + + return post +``` + diff --git a/specs/_features/epbs/p2p-interface.md b/specs/_features/epbs/p2p-interface.md new file mode 100644 index 0000000000..83228347d4 --- /dev/null +++ b/specs/_features/epbs/p2p-interface.md @@ -0,0 +1,280 @@ +# ePBS -- Networking + +This document contains the consensus-layer networking specification for ePBS. + + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Modification in ePBS](#modification-in-epbs) + - [Preset](#preset) + - [Containers](#containers) + - [`BlobSidecar`](#blobsidecar) + - [Helpers](#helpers) + - [Modified `verify_blob_sidecar_inclusion_proof`](#modified-verify_blob_sidecar_inclusion_proof) + - [The gossip domain: gossipsub](#the-gossip-domain-gossipsub) + - [Topics and messages](#topics-and-messages) + - [Global topics](#global-topics) + - [`beacon_block`](#beacon_block) + - [`execution_payload`](#execution_payload) + - [`payload_attestation_message`](#payload_attestation_message) + - [`execution_payload_header`](#execution_payload_header) + - [`inclusion_list`](#inclusion_list) + - [Transitioning the gossip](#transitioning-the-gossip) + - [The Req/Resp domain](#the-reqresp-domain) + - [Messages](#messages) + - [BeaconBlocksByRange v3](#beaconblocksbyrange-v3) + - [BeaconBlocksByRoot v3](#beaconblocksbyroot-v3) + - [BlobSidecarsByRoot v2](#blobsidecarsbyroot-v2) + - [ExecutionPayloadEnvelopeByRoot v1](#executionpayloadenvelopebyroot-v1) + + + +## Modification in ePBS + +### Preset + +*[Modified in ePBS]* + +| Name | Value | Description | +|------------------------------------------|-----------------------------------|---------------------------------------------------------------------| +| `KZG_COMMITMENT_INCLUSION_PROOF_DEPTH` | TODO: Compute it when the spec stabilizes | Merkle proof depth for the `blob_kzg_commitments` list item | +| `KZG_GENERALIZED_INDEX_PREFIX` | TODO: Compute it when the spec stabilizes | Generalized index for the first item in the `blob_kzg_commitments` list | + +### Containers + +#### `BlobSidecar` + +The `BlobSidecar` container is modified indirectly because the constant `KZG_COMMITMENT_INCLUSION_PROOF_DEPTH` is modified. + +```python +class BlobSidecar(Container): + index: BlobIndex # Index of blob in block + blob: Blob + kzg_commitment: KZGCommitment + kzg_proof: KZGProof # Allows for quick verification of kzg_commitment + signed_block_header: SignedBeaconBlockHeader + kzg_commitment_inclusion_proof: Vector[Bytes32, KZG_COMMITMENT_INCLUSION_PROOF_DEPTH] +``` + +#### Helpers + +##### Modified `verify_blob_sidecar_inclusion_proof` + +`verify_blob_sidecar_inclusion_proof` is modified in ePBS to account for the fact that the kzg commitments are included in the `ExecutionPayloadEnvelope` and no longer in the beacon block body. + +```python +def verify_blob_sidecar_inclusion_proof(blob_sidecar: BlobSidecar) -> bool: + # hardcoded here because the block does not include the commitments but only their root. + gindex = GeneralizedIndex(KZG_GENERALIZED_INDEX_PREFIX + blob_sidecar.index) + + return is_valid_merkle_branch( + leaf=blob_sidecar.kzg_commitment.hash_tree_root(), + branch=blob_sidecar.kzg_commitment_inclusion_proof, + depth=KZG_COMMITMENT_INCLUSION_PROOF_DEPTH, + index=gindex, + root=blob_sidecar.signed_block_header.message.body_root, + ) +``` + +### The gossip domain: gossipsub + +Some gossip meshes are upgraded in the fork of ePBS to support upgraded types. + +#### Topics and messages + +Topics follow the same specification as in prior upgrades. + +The `beacon_block` topic is updated to support the modified type +| Name | Message Type | +| --- | --- | +| `beacon_block` | `SignedBeaconBlock` (modified) | + +The new topics along with the type of the `data` field of a gossipsub message are given in this table: + +| Name | Message Type | +|-------------------------------|------------------------------------------------------| +| `execution_payload_header` | `SignedExecutionPayloadHeader` [New in ePBS] | +| `execution_payload` | `SignedExecutionPayloadEnvelope` [New in ePBS] | +| `payload_attestation_message` | `PayloadAttestationMessage` [New in ePBS] | +| `inclusion_list` | `InclusionList` [New in ePBS] | + +##### Global topics + +ePBS introduces new global topics for execution header, execution payload, payload attestation and inclusion list. + +###### `beacon_block` + +[Modified in ePBS] + +The *type* of the payload of this topic changes to the (modified) `SignedBeaconBlock` found in ePBS spec. + +There are no new validations for this topic. However, all validations with regards to the `ExecutionPayload` are removed: + +- _[REJECT]_ The length of KZG commitments is less than or equal to the limitation defined in Consensus Layer -- i.e. validate that len(body.signed_beacon_block.message.blob_kzg_commitments) <= MAX_BLOBS_PER_BLOCK +- _[REJECT]_ The block's execution payload timestamp is correct with respect to the slot + -- i.e. `execution_payload.timestamp == compute_timestamp_at_slot(state, block.slot)`. +- If `execution_payload` verification of block's parent by an execution node is *not* complete: + - [REJECT] The block's parent (defined by `block.parent_root`) passes all validation (excluding execution node verification of the `block.body.execution_payload`). +- otherwise: + - [IGNORE] The block's parent (defined by `block.parent_root`) passes all validation (including execution node verification of the `block.body.execution_payload`). +- [REJECT] The block's parent (defined by `block.parent_root`) passes validation. + +And instead the following validations are set in place with the alias `header = signed_execution_payload_header.message`: + +- If `execution_payload` verification of block's execution payload parent by an execution node **is complete**: + - [REJECT] The block's execution payload parent (defined by `header.parent_block_hash`) passes all validation. +- [REJECT] The block's parent (defined by `block.parent_root`) passes validation. + +###### `execution_payload` + +This topic is used to propagate execution payload messages as `SignedExecutionPayloadEnvelope`. + +The following validations MUST pass before forwarding the `signed_execution_payload_envelope` on the network, assuming the alias `envelope = signed_execution_payload_envelope.message`, `payload = payload_envelope.payload`: + +- _[IGNORE]_ The envelope's block root `envelope.block_root` has been seen (via both gossip and non-gossip sources) (a client MAY queue payload for processing once the block is retrieved). +- _[IGNORE]_ The node has not seen another valid `SignedExecutionPayloadEnvelope` for this block root from this builder. + +Let `block` be the block with `envelope.beacon_block_root`. +Let `header` alias `block.body.signed_execution_payload_header.message` (notice that this can be obtained from the `state.signed_execution_payload_header`) +- _[REJECT]_ `block` passes validation. +- _[REJECT]_ `envelope.builder_index == header.builder_index` +- if `envelope.payload_withheld == False` then + - _[REJECT]_ `payload.block_hash == header.block_hash` +- _[REJECT]_ The builder signature, `signed_execution_payload_envelope.signature`, is valid with respect to the builder's public key. + +###### `payload_attestation_message` + +This topic is used to propagate signed payload attestation message. + +The following validations MUST pass before forwarding the `payload_attestation_message` on the network, assuming the alias `data = payload_attestation_message.data`: + +- _[IGNORE]_ `data.slot` is the current slot. +- _[REJECT]_ `data.payload_status < PAYLOAD_INVALID_STATUS` +- _[IGNORE]_ the `payload_attestation_message` is the first valid payload attestation message received from the validator index. +- _[IGNORE]_ The attestation's `data.beacon_block_root` has been seen (via both gossip and non-gossip sources) (a client MAY queue attestation for processing once the block is retrieved. Note a client might want to request payload after). +_ _[REJECT]_ The beacon block with root `data.beacon_block_root` passes validation. +- _[REJECT]_ The validator index is within the payload committee in `get_ptc(state, data.slot)`. For the current's slot head state. +- _[REJECT]_ The signature of `payload_attestation_message.signature` is valid with respect to the validator index. + +###### `execution_payload_header` + +This topic is used to propagate signed bids as `SignedExecutionPayloadHeader`. + +The following validations MUST pass before forwarding the `signed_execution_payload_header` on the network, assuming the alias `header = signed_execution_payload_header.message`: + +- _[IGNORE]_ this is the first signed bid seen with a valid signature from the given builder for this slot. +- _[IGNORE]_ this bid is the highest value bid seen for the pair of the corresponding slot and the given parent block hash. +- _[REJECT]_ The signed builder bid, `header.builder_index` is a valid and non-slashed builder index in state. +- _[IGNORE]_ The signed builder bid value, `header.value`, is less or equal than the builder's balance in state. i.e. `MIN_BUILDER_BALANCE + header.value < state.builder_balances[header.builder_index]`. +- _[IGNORE]_ `header.parent_block_hash` is the block hash of a known execution payload in fork choice. +- _[IGNORE]_ `header.slot` is the current slot or the next slot. +- _[REJECT]_ The builder signature, `signed_execution_payload_header_envelope.signature`, is valid with respect to the `header_envelope.builder_index`. + +###### `inclusion_list` + +This topic is used to propagate inclusion lists as `InclusionList` objects. + +The following validations MUST pass before forwarding the `inclusion_list` on the network, assuming the alias `signed_summary = inclusion_list.signed_summary`, `summary = signed_summary.message`: + +- _[IGNORE]_ The inclusion list is for the current slot or the next slot (a client MAY queue future inclusion lists for processing at the appropriate slot). +- _[IGNORE]_ The inclusion list is the first valid inclusion list received for the proposer for the slot, `inclusion_list.slot`. +- _[REJECT]_ The inclusion list transactions `inclusion_list.transactions` length is less or equal than `MAX_TRANSACTIONS_PER_INCLUSION_LIST`. +- _[REJECT]_ The inclusion list summary has the same length of transactions `len(summary.summary) == len(inclusion_list.transactions)`. +- _[REJECT]_ The summary signature, `signed_summary.signature`, is valid with respect to the `proposer_index` pubkey. +- _[REJECT]_ The summary is proposed by the expected proposer_index for the summary's slot in the context of the current shuffling (defined by parent_root/slot). If the proposer_index cannot immediately be verified against the expected shuffling, the inclusion list MAY be queued for later processing while proposers for the summary's branch are calculated -- in such a case do not REJECT, instead IGNORE this message. + +#### Transitioning the gossip + +See gossip transition details found in the [Altair document](../altair/p2p-interface.md#transitioning-the-gossip) for +details on how to handle transitioning gossip topics for this upgrade. + +### The Req/Resp domain + +#### Messages + +##### BeaconBlocksByRange v3 + +**Protocol ID:** `/eth2/beacon_chain/req/beacon_blocks_by_range/3/` + +[0]: # (eth2spec: skip) + +| `fork_version` | Chunk SSZ type | +|--------------------------|-------------------------------| +| `GENESIS_FORK_VERSION` | `phase0.SignedBeaconBlock` | +| `ALTAIR_FORK_VERSION` | `altair.SignedBeaconBlock` | +| `BELLATRIX_FORK_VERSION` | `bellatrix.SignedBeaconBlock` | +| `CAPELLA_FORK_VERSION` | `capella.SignedBeaconBlock` | +| `DENEB_FORK_VERSION` | `deneb.SignedBeaconBlock` | +| `EPBS_FORK_VERSION` | `epbs.SignedBeaconBlock` | + +##### BeaconBlocksByRoot v3 + +**Protocol ID:** `/eth2/beacon_chain/req/beacon_blocks_by_root/3/` + +Per `context = compute_fork_digest(fork_version, genesis_validators_root)`: + +[1]: # (eth2spec: skip) + +| `fork_version` | Chunk SSZ type | +|--------------------------|-------------------------------| +| `GENESIS_FORK_VERSION` | `phase0.SignedBeaconBlock` | +| `ALTAIR_FORK_VERSION` | `altair.SignedBeaconBlock` | +| `BELLATRIX_FORK_VERSION` | `bellatrix.SignedBeaconBlock` | +| `CAPELLA_FORK_VERSION` | `capella.SignedBeaconBlock` | +| `DENEB_FORK_VERSION` | `deneb.SignedBeaconBlock` | +| `EPBS_FORK_VERSION` | `epbs.SignedBeaconBlock` | + + +##### BlobSidecarsByRoot v2 + +**Protocol ID:** `/eth2/beacon_chain/req/blob_sidecars_by_root/2/` + +[1]: # (eth2spec: skip) + +| `fork_version` | Chunk SSZ type | +|--------------------------|-------------------------------| +| `DENEB_FORK_VERSION` | `deneb.BlobSidecar` | +| `EPBS_FORK_VERSION` | `epbs.BlobSidecar` | + + +##### ExecutionPayloadEnvelopeByRoot v1 + +**Protocol ID:** `/eth2/beacon_chain/req/execution_payload_envelope_by_root/1/` + +The `` field is calculated as `context = compute_fork_digest(fork_version, genesis_validators_root)`: + +[1]: # (eth2spec: skip) + +| `fork_version` | Chunk SSZ type | +|---------------------|---------------------------------------| +| `EPBS_FORK_VERSION` | `epbs.SignedExecutionPayloadEnvelope` | + +Request Content: + +``` +( + List[Root, MAX_REQUEST_PAYLOAD] +) +``` + +Response Content: + +``` +( + List[SignedExecutionPayloadEnvelope, MAX_REQUEST_PAYLOAD] +) +``` +Requests execution payload envelope by `signed_execution_payload_envelope.message.block_root`. The response is a list of SignedExecutionPayloadEnvelope whose length is less than or equal to the number of requested execution payload envelopes. It may be less in the case that the responding peer is missing payload envelopes. + +No more than MAX_REQUEST_PAYLOAD may be requested at a time. + +ExecutionPayloadEnvelopeByRoot is primarily used to recover recent execution payload envelope (e.g. when receiving a payload attestation with revealed status as true but never received a payload). + +The request MUST be encoded as an SSZ-field. + +The response MUST consist of zero or more response_chunk. Each successful response_chunk MUST contain a single SignedExecutionPayloadEnvelope payload. + +Clients MUST support requesting payload envelopes since the latest finalized epoch. + +Clients MUST respond with at least one payload envelope, if they have it. Clients MAY limit the number of payload envelopes in the response. diff --git a/specs/_features/epbs/validator.md b/specs/_features/epbs/validator.md new file mode 100644 index 0000000000..ed37d8aaae --- /dev/null +++ b/specs/_features/epbs/validator.md @@ -0,0 +1,185 @@ +# ePBS -- Honest Validator + +This document represents the changes and additions to the Honest validator guide included in the ePBS fork. + + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Protocols](#protocols) + - [`ExecutionEngine`](#executionengine) + - [`get_execution_inclusion_list`](#get_execution_inclusion_list) +- [Validator assignment](#validator-assignment) + - [Lookahead](#lookahead) +- [Beacon chain responsibilities](#beacon-chain-responsibilities) + - [Block proposal](#block-proposal) + - [Inclusion lists generation](#inclusion-lists-generation) + - [Constructing the new `signed_execution_payload_header_envelope` field in `BeaconBlockBody`](#constructing-the-new-signed_execution_payload_header_envelope-field-in--beaconblockbody) + - [Constructing the new `payload_attestations` field in `BeaconBlockBody`](#constructing-the-new-payload_attestations-field-in--beaconblockbody) + - [Blobs sidecars](#blobs-sidecars) + - [Payload timeliness attestation](#payload-timeliness-attestation) + - [Constructing a payload attestation](#constructing-a-payload-attestation) + + + +## Protocols + +### `ExecutionEngine` + +#### `get_execution_inclusion_list` + +*Note*: `get_execution_inclusion_list` function is added to the `ExecutionEngine` protocol for use as a validator. + +```python +class GetInclusionListResponse(container) + summary: List[ExecutionAddress, MAX_TRANSACTIONS_PER_INCLUSION_LIST] + transactions: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] +``` + +```python +def get_execution_inclusion_list(self: ExecutionEngine, parent_block_hash: Root) -> GetInclusionListResponse: + """ + Return ``GetInclusionListResponse`` object. + """ + ... +``` + +Given the `parent_block_hash`, `get_execution_inclusion_list` returns `GetInclusionListResponse` with the most recent version of the inclusion list based on the parent block hash. + +The body of this function is implementation dependent. The Engine API may be used to implement it with an external execution engine. This function returns a `GetInclusionListResponse` object that has to have the following properties +- The `transactions` list consists of a list of transactions that can be executed based on the current execution state parametrized by the `parent_block_hash`. This includes validations that each of the "from" addresses has enough funds to cover the maximum gas specified in the transactions at the specified `base_fee_per_gas` and any possible increase for the next block because of EIP-1559. It also includes checks for nonce correctness. +- The total gas limit of all transactions in the list is less or equal than `MAX_GAS_PER_INCLUSION_LIST`. +- The `summary` consists of the "from" addresses from the `transactions` list. In particular, these lists have the same length. + +## Validator assignment + +A validator can get PTC assignments for a given slot using the following helper via `get_ptc_assignment(state, epoch, validator_index)` where `epoch <= next_epoch`. + +A validator can use the following function to see if they are supposed to submit payload attestation message during a slot across an epoch. +PTC committee selection is only stable within the context of the current and next epoch. + +```python +def get_ptc_assignment(state: BeaconState, + epoch: Epoch, + validator_index: ValidatorIndex + ) -> Optional[Slot]: + """ + Return the ptc committee assignment in the ``slot`` for ``validator_index``. + ``assignment`` returned is a tuple of the following form: + * ``assignment[0]`` is the list of validators in the ptc + * ``assignment[1]`` is the slot at which the ptc is assigned + Return None if no assignment. + """ + 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 +``` + +### Lookahead + +[New in ePBS] + +`get_ptc_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 ptc committee +slot and planning to participate in the ptc committee subnet. + +## Beacon chain responsibilities + +All validator responsibilities remain unchanged other than the following: + +- Proposers are required to broadcast `InclusionList` objects in addition to the `SignedBeaconBlock` objects. +- Proposers are no longer required to broadcast `BlobSidecar` objects, as this becomes a builder's duty. +- Some validators are selected per slot to become PTC members, these validators must broadcat `PayloadAttestationMessage` objects + + +### Block proposal + +Validators 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 + +#### Inclusion lists generation +ePBS introduces forward inclusion lists. The proposer must construct and broadcast an `InclusionList` object alongside `SignedBeaconBlock`. + +To obtain an inclusion list, a block proposer building a block on top of a `state` must take the following actions: + +1. If `is_parent_block_full(state)` returns `False`, the proposer should not broadcast an inclusion list. Any inclusion list for this state will be ignored by validators. + +2. Retrieve inclusion list from execution layer by calling `get_execution_inclusion_list`. + +3. Call `build_inclusion_list` to build `InclusionList`. + +```python +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) +``` + +```python +def build_inclusion_list(state: BeaconState, inclusion_list_response: GetInclusionListResponse, block_slot: Slot, parent_block_hash: Hash32, index: ValidatorIndex, privkey: int) -> InclusionList: + summary = InclusionListSummary(proposer_index=index, slot=block_slot, summary=inclusion_list_response.summary) + signature = get_inclusion_list_summary_signature(state, summary, privkey) + signed_summary = SignedInclusionListSummary(message=summary, signature=signature) + return InclusionList(signed_summary=signed_inclusion_list_summary, parent_block_hash=parent_block_hash, transactions=inclusion_list_response.transactions) +``` + +#### Constructing the new `signed_execution_payload_header_envelope` field in `BeaconBlockBody` + +To obtain `signed_execution_payload_header`, a block proposer building a block on top of a `state` must take the following actions: +* Listen to the `execution_payload_header` gossip global topic and save an accepted `signed_execution_payload_header` from a builder. Proposer MAY obtain these signed messages by other off-protocol means. +* The `signed_execution_payload_header` must satisfy the verification conditions found in `process_execution_payload_header`, that is + - The header signature must be valid + - The builder balance can cover the header value + - The header slot is for the proposal block slot + - The header parent block hash equals the state's `latest_block_hash`. +* Select one bid and set `body.signed_execution_payload_header = signed_execution_payload_header` + +#### Constructing the new `payload_attestations` field in `BeaconBlockBody` + +Up to `MAX_PAYLOAD_ATTESTATIONS`, aggregate payload attestations can be included in the block. The validator will have to +* Listen to the `payload_attestation_message` gossip global topic +* The payload attestations added must satisfy the verification conditions found in payload attestation gossip validation and payload attestation processing. This means + - The `data.beacon_block_root` corresponds to `block.parent_root`. + - The slot of the parent block is exactly one slot before the proposing slot. + - The signature of the payload attestation data message verifies correctly. +* The proposer needs to aggregate all payload attestations with the same data into a given `PayloadAttestation` object. For this it needs to fill the `aggregation_bits` field by using the relative position of the validator indices with respect to the PTC that is obtained from `get_ptc(state, block_slot - 1)`. +* Proposer should only include payload attestations that are consistent with the current block they are proposing. That is, if the previous block had a payload, they should only include attestations with `payload_status = PAYLOAD_PRESENT`. Proposers are penalized for attestations that are not-consistent with their view. + +#### Blobs sidecars +The blbos sidecars are no longer broadcast by the validator, and thus their construction is not necessary. This deprecates the corresponding sections from the honest validator guide in the Deneb fork, moving them, albeit with some modifications, to the [honest Builder guide](./builder.md) + +### Payload timeliness attestation + +Some validators are selected to submit payload timeliness attestation. Validators should call `get_ptc_assignment` at the beginning of an epoch to be prepared to submit their PTC attestations during the next epoch. + +A validator should create and broadcast the `payload_attestation_message` to the global execution attestation subnet at `SECONDS_PER_SLOT * 3 / INTERVALS_PER_SLOT` seconds after the start of `slot` + +#### Constructing a payload attestation + +If a validator is in the payload attestation committee (i.e. `is_assigned_to_payload_committee()` below returns True), +then the validator should prepare a `PayloadAttestationMessage` for the current slot, +according to the logic in `get_payload_attestation_message` at `SECONDS_PER_SLOT * 3 / INTERVALS_PER_SLOT` interval, and broadcast it to the global `payload_attestation_message` pubsub topic. + +the validator creates `payload_attestation_message` as follows: +* If the validator has not seen any beacon block for the assigned slot, do not submit a payload attestation. It will be ignored anyway. +* Set `data.beacon_block_root` be the HTR of the beacon block seen for the assigned slot +* Set `data.slot` to be the assigned slot. +* Set `data.payload_status` as follows + - If a valid `SignedExecutionPayloadEnvelope` has been seen referencing the block `data.beacon_block_root` and the envelope has `payload_withheld = False`, set to `PAYLOAD_PRESENT`. + - If a valid `SignedExecutionPayloadEnvelope` has been seen referencing the block `data.beacon_block_root` and the envelope has `payload_withheld = True`, set to `PAYLOAD_WITHHELD`. + - If no valid `SignedExecutionPayloadEnvelope` has been seen referencing the block `data.beacon_block_root` set to `PAYLOAD_ABSENT`. +* Set `payload_attestation_message.validator_index = validator_index` where `validator_index` is the validator chosen to submit. The private key mapping to `state.validators[validator_index].pubkey` is used to sign the payload timeliness attestation. +* Sign the `payload_attestation_message.data` using the helper `get_payload_attestation_message_signature`. + +Notice that the attester only signs the `PayloadAttestationData` and not the `validator_index` field in the message. Proposers need to aggregate these attestations as described above. + +```python +def get_payload_attestation_message_signature(state: BeaconState, attestation: PayloadAttestationMessage, privkey: int) -> BLSSignature: + domain = get_domain(state, DOMAIN_PTC_ATTESTER, compute_epoch_at_slot(attestation.data.slot)) + signing_root = compute_signing_root(attestation.data, domain) + return bls.Sign(privkey, signing_root) +``` diff --git a/specs/_features/epbs/weak-subjectivity.md b/specs/_features/epbs/weak-subjectivity.md new file mode 100644 index 0000000000..19bd08068b --- /dev/null +++ b/specs/_features/epbs/weak-subjectivity.md @@ -0,0 +1,56 @@ +# ePBS -- Weak Subjectivity Guide + +## Table of contents + + + + + +- [Weak Subjectivity Period](#weak-subjectivity-period) + - [Calculating the Weak Subjectivity Period](#calculating-the-weak-subjectivity-period) + - [Modified `compute_weak_subjectivity_period`](#modified-compute_weak_subjectivity_period) + + + +## Weak Subjectivity Period + +### Calculating the Weak Subjectivity Period + +#### Modified `compute_weak_subjectivity_period` +**Note:** The function `compute_weak_subjectivity_period` is modified to use the modified churn in ePBS. + +```python +def compute_weak_subjectivity_period(state: BeaconState) -> uint64: + """ + Returns the weak subjectivity period for the current ``state``. + This computation takes into account the effect of: + - validator set churn (bounded by ``get_validator_churn_limit()`` per epoch), and + - validator balance top-ups (bounded by ``MAX_DEPOSITS * SLOTS_PER_EPOCH`` per epoch). + A detailed calculation can be found at: + https://github.com/runtimeverification/beacon-chain-verification/blob/master/weak-subjectivity/weak-subjectivity-analysis.pdf + """ + ws_period = MIN_VALIDATOR_WITHDRAWABILITY_DELAY + N = len(get_active_validator_indices(state, get_current_epoch(state))) + t = get_total_active_balance(state) // N // ETH_TO_GWEI + T = MAX_EFFECTIVE_BALANCE // ETH_TO_GWEI + delta = get_validator_churn_limit(state) // MIN_ACTIVATION_BALANCE + Delta = MAX_DEPOSITS * SLOTS_PER_EPOCH + D = SAFETY_DECAY + + if T * (200 + 3 * D) < t * (200 + 12 * D): + epochs_for_validator_set_churn = ( + N * (t * (200 + 12 * D) - T * (200 + 3 * D)) // (600 * delta * (2 * t + T)) + ) + epochs_for_balance_top_ups = ( + N * (200 + 3 * D) // (600 * Delta) + ) + ws_period += max(epochs_for_validator_set_churn, epochs_for_balance_top_ups) + else: + ws_period += ( + 3 * N * D * t // (200 * Delta * (T - t)) + ) + + return ws_period +``` + +