From 5ef39b811118d939b04aa64f7fd01e7a0308dee2 Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 7 Aug 2023 11:22:37 -0300 Subject: [PATCH 001/112] init --- specs/_features/epbs/beacon-chain.md | 331 +++++++++++++++++++++++++++ 1 file changed, 331 insertions(+) create mode 100644 specs/_features/epbs/beacon-chain.md diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md new file mode 100644 index 0000000000..8d293e7bfc --- /dev/null +++ b/specs/_features/epbs/beacon-chain.md @@ -0,0 +1,331 @@ +# ePBS -- The Beacon Chain + +## Table of contents + + + + + + + + +## 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 as opposed to the current three. Honest validators gather *signed bids* from builders and submit their consensus blocks (a `SignedBlindedBeaconBlock`) 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 *full* block from a previous slot (eg. the current slot's proposer did not submit its block). +- An *empty* block from the current slot (eg. 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). + +For a further introduction please refer to this [ethresear.ch article](https://ethresear.ch/t/payload-timeliness-committee-ptc-an-epbs-design/16054) + +## Preset + +### Misc + +| Name | Value | +| - | - | +| `PTC_SIZE` | `uint64(2**9)` (=512) | + +### Domain types + +| Name | Value | +| - | - | +| `DOMAIN_BEACON_BUILDER` | `DomainType('0x0B000000')` | + +### State list lengths + +| Name | Value | Unit | Duration | +| - | - | :-: | :-: | +| `BUILDER_REGISTRY_LIMIT` | `uint64(2**20)` (=1,048,576) | builders | + +### Gwei values + +| Name | Value | +| - | - | +| `BUILDER_MIN_BALANCE` | `Gwei(2**10 * 10**9)` = (1,024,000,000,000) | + +### Incentivization weights + +| Name | Value | +| - | - | +| `PTC_PENALTY_WEIGHT` | `uint64(2)` | + +### Execution +| Name | Value | +| - | - | +| MAX_TRANSACTIONS_PER_INCLUSION_LIST | `2**4` (=16) | + +## Containers + +### New Containers + +#### `Builder` + +``` python +class Builder(Container): + pubkey: BLSPubkey + withdrawal_address: ExecutionAddress # Commitment to pubkey for withdrawals + effective_balance: Gwei # Balance at stake + exit_epoch: Epoch + withdrawable_epoch: Epoch # When builder can withdraw funds +``` + +#### `SignedExecutionPayloadHeader` + +```python +class SignedExecutionPayloadHeader(Container): + message: ExecutionPayloadHeader + signature: BLSSignature +``` + +#### `ExecutionPayloadEnvelope` + +```python +class ExecutionPayloadEnvelope(Container): + payload: ExecutionPayload + state_root: Root +``` + +#### `SignedExecutionPayloadEnvelope` + +```python +class SignedExecutionPayloadEnvelope(Container): + message: ExecutionPayloadEnvelope + signature: BLSSignature +``` + +### Modified Containers + +#### `ExecutionPayload` + +```python +class ExecutionPayload(Container): + # Execution block header fields + parent_hash: Hash32 + fee_recipient: ExecutionAddress # 'beneficiary' in the yellow paper + state_root: Bytes32 + receipts_root: Bytes32 + logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] + prev_randao: Bytes32 # 'difficulty' in the yellow paper + block_number: uint64 # 'number' in the yellow paper + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + # Extra payload fields + block_hash: Hash32 # Hash of execution block + transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD] + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + builder_index: uint64 # [New in ePBS] +``` + +#### `ExecutionPayloadHeader` + +```python +class ExecutionPayloadHeader(Container): + # Execution block header fields + parent_hash: Hash32 + fee_recipient: ExecutionAddress + state_root: Bytes32 + receipts_root: Bytes32 + logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] + prev_randao: Bytes32 + block_number: uint64 + gas_limit: uint64 + gas_used: uint64 + timestamp: uint64 + extra_data: ByteList[MAX_EXTRA_DATA_BYTES] + base_fee_per_gas: uint256 + # Extra payload fields + block_hash: Hash32 # Hash of execution block + transactions_root: Root + withdrawals_root: Root + builder_index: uint64 # [New in ePBS] +``` + +#### `BeaconBlockBody` + +```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_payload_header: SignedExecutionPayloadHeader # [Modified in ePBS] + bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] + tx_inclusion_list: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] +``` + + +#### `BeaconState` +*Note*: the beacon state is modified to store a signed latest execution payload header and it adds a registry of builders, their balances and two transaction inclusion lists. + +```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 + # Withdrawals + next_withdrawal_index: WithdrawalIndex + next_withdrawal_validator_index: ValidatorIndex + # Deep history valid from Capella onwards + historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] + # PBS + builders: List[Builder, BUILDER_REGISTRY_LIMIT] # [New in ePBS] + builder_balances: List[Gwei, BUILDER_REGISTRY_LIMIT] # [New in ePBS] + previous_tx_inclusion_list: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] # [New in ePBS] + current_tx_inclusion_list: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] # [New in ePBS] + current_signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] +``` + +## 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`. + +```python +def process_block(state: BeaconState, block: BeaconBlock) -> None: + process_block_header(state, block) + process_execution_payload_header(state, block.body.execution_payload_header) # [Modified in ePBS] + # Removed process_withdrawal in ePBS is processed during payload processing [Modified in ePBS] + 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) + process_tx_inclusion_list(state, block) # [New in ePBS] +``` + +#### New `update_tx_inclusion_lists` + +```python +def update_tx_inclusion_lists(state: BeaconState, payload: ExecutionPayload) -> None: + old_transactions = payload.transactions[:len(state.previous_tx_inclusion_list)] + assert state.previous_tx_inclusion_list == old_transactions + + new_transactions = payload.transactions[len(state.previous_tx_inclusion_list):] + state.previous_tx_inclusion_list = [tx for tx in state.current_tx_inclusion_list if x not in new_transactions] + + #TODO: check validity of the IL for the next block, requires engine changes +``` +#### New `verify_execution_payload_header_signature` + +```python +def verify_execution_payload_header_signature(state: BeaconState, signed_header: SignedExecutionPayloadHeader) -> bool: + builder = state.builders[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 `verify_execution_payload_signature` + +```python +def verify_execution_envelope_signature(state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope) -> bool: + builder = state.builders[signed_envelope.message.payload.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 `process_execution_payload_header` + +```python +def process_execution_payload_header(state: BeaconState, signed_header: SignedExecutionPayloadHeader) -> None: + assert verify_execution_payload_header_signature(state, signed_header) + header = signed_header.message + # Verify consistency of the parent hash with respect to the previous execution payload header + assert header.parent_hash == state.latest_execution_payload_header.block_hash + # Verify prev_randao + assert header.prev_randao == get_randao_mix(state, get_current_epoch(state)) + # Verify timestamp + assert header.timestamp == compute_timestamp_at_slot(state, state.slot) + # Cache execution payload header + state.current_signed_execution_payload_header = signed_header +``` + +#### 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. + +TODO: Deal with the case when the payload becomes invalid because of the forward inclusion list. + +```python +def process_execution_payload(state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope, execution_engine: ExecutionEngine) -> None: + # Verify signature [New in ePBS] + assert verify_execution_envelope_signature(state, signed_envelope) + payload = signed_envelope.message.payload + # Verify consistency with the committed header + hash = hash_tree_root(payload) + previous_hash = hash_tree_root(state.current_signed_execution_payload_header.message) + assert hash == previous_hash + # Verify and update the proposers inclusion lists + update_tx_inclusion_lists(state, payload) + # Verify the execution payload is valid + assert execution_engine.verify_and_notify_new_payload(NewPayloadRequest(execution_payload=payload)) + # Process Withdrawals in the payload + process_withdrawals(state, payload) + # Cache the execution payload header + state.latest_execution_payload_header = state.current_signed_execution_payload_header.message + # Verify the state root + assert signed_envelope.message.state_root == hash_tree_root(state) +``` + +#### New `process_tx_inclusion_list` + +```python +def process_tx_inclusion_list(state: BeaconState, block: BeaconBlock) -> None: + # TODO: cap gas usage, comunicate with the engine. + state.previous_tx_inclusion_list = state.current_tx_inclusion_list + state.current_tx_inclusion_list = block.body.tx_inclusion_list +``` + From 1a49a5108a52461ecdf07d4a63a1db6e7d7bdc0c Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 8 Aug 2023 11:54:34 -0300 Subject: [PATCH 002/112] Add engine methods and check IL validity --- specs/_features/epbs/beacon-chain.md | 94 ++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 13 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 8d293e7bfc..7383485ddb 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -60,6 +60,7 @@ For a further introduction please refer to this [ethresear.ch article](https://e | Name | Value | | - | - | | MAX_TRANSACTIONS_PER_INCLUSION_LIST | `2**4` (=16) | +| MAX_GAS_PER_INCLUSION_LIST | `2**20` (=1,048,576) | ## Containers @@ -124,6 +125,7 @@ class ExecutionPayload(Container): transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD] withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] builder_index: uint64 # [New in ePBS] + value: Gwei # [New in ePBS] ``` #### `ExecutionPayloadHeader` @@ -148,6 +150,7 @@ class ExecutionPayloadHeader(Container): transactions_root: Root withdrawals_root: Root builder_index: uint64 # [New in ePBS] + value: Gwei # [New in ePBS] ``` #### `BeaconBlockBody` @@ -232,41 +235,101 @@ The post-state corresponding to a pre-state `state` and a signed block `signed_b 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. +### Execution engine + +#### Request data + +##### New `NewInclusionListRequest` + +```python +@dataclass +class NewInclusionListRequest(object): + inclusion_list: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] +``` + +#### Engine APIs + +#### Modified `notify_new_payload` +*Note*: the function notify new payload is modified to raise an exception if the payload is not valid, and to return the list of transactions that remain valid in the inclusion list + +```python +def notify_new_payload(self: ExecutionEngine, execution_payload: ExecutionPayload) -> List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST]: + """ + Raise an exception if ``execution_payload`` is not valid with respect to ``self.execution_state``. + Returns the list of transactions in the inclusion list that remain valid after executing the payload. That is + it is guaranteed that the transactions returned in the list can be executed in the exact order starting from the + current ``self.execution_state``. + """ + ... +``` + +#### Modified `verify_and_notify_new_payload` +*Note*: the function `verify_and_notify_new_payload` is modified so that it returns the list of transactions that remain valid in the forward inclusion list. It raises an exception if the payload is not valid. + +```python +def verify_and_notify_new_payload(self: ExecutionEngine, + new_payload_request: NewPayloadRequest) -> List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST]: + """ + Raise an exception if ``execution_payload`` is not valid with respect to ``self.execution_state``. + Returns the list of transactions in the inclusion list that remain valid after executing the payload. That is + it is guaranteed that the transactions returned in the list can be executed in the exact order starting from the + current ``self.execution_state``. + """ + assert self.is_valid_block_hash(new_payload_request.execution_payload) + return self.notify_new_payload(new_payload_request.execution_payload) +``` + +#### 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 current ``self.execution_state`` and that they consume less or equal than + ```MAX_GAS_PER_INCLUSION_LIST``. + """ + ... +``` + ### 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 `process_tx_inclusion_list` needs to be processed before the payload header since the former requires to check the last committed payload header. + + ```python def process_block(state: BeaconState, block: BeaconBlock) -> None: process_block_header(state, block) + process_tx_inclusion_list(state, block, EXECUTION_ENGINE) # [New in ePBS] process_execution_payload_header(state, block.body.execution_payload_header) # [Modified in ePBS] # Removed process_withdrawal in ePBS is processed during payload processing [Modified in ePBS] 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) - process_tx_inclusion_list(state, block) # [New in ePBS] ``` #### New `update_tx_inclusion_lists` ```python -def update_tx_inclusion_lists(state: BeaconState, payload: ExecutionPayload) -> None: +def update_tx_inclusion_lists(state: BeaconState, payload: ExecutionPayload, engine: ExecutionEngine, inclusion_list: List[Transaction, MAX_TRANSACTION_PER_INCLUSION_LIST]) -> None: old_transactions = payload.transactions[:len(state.previous_tx_inclusion_list)] assert state.previous_tx_inclusion_list == old_transactions - new_transactions = payload.transactions[len(state.previous_tx_inclusion_list):] - state.previous_tx_inclusion_list = [tx for tx in state.current_tx_inclusion_list if x not in new_transactions] - - #TODO: check validity of the IL for the next block, requires engine changes + state.previous_tx_inclusion_list = inclusion_list ``` + #### New `verify_execution_payload_header_signature` ```python def verify_execution_payload_header_signature(state: BeaconState, signed_header: SignedExecutionPayloadHeader) -> bool: builder = state.builders[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) + if not bls.Verify(builder.pubkey, signing_root, signed_header.signature): + return False + return ``` #### New `verify_execution_payload_signature` @@ -308,10 +371,10 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti hash = hash_tree_root(payload) previous_hash = hash_tree_root(state.current_signed_execution_payload_header.message) assert hash == previous_hash - # Verify and update the proposers inclusion lists - update_tx_inclusion_lists(state, payload) # Verify the execution payload is valid - assert execution_engine.verify_and_notify_new_payload(NewPayloadRequest(execution_payload=payload)) + inclusion_list = execution_engine.verify_and_notify_new_payload(NewPayloadRequest(execution_payload=payload)) + # Verify and update the proposers inclusion lists + update_tx_inclusion_lists(state, payload, inclusion_list) # Process Withdrawals in the payload process_withdrawals(state, payload) # Cache the execution payload header @@ -323,9 +386,14 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti #### New `process_tx_inclusion_list` ```python -def process_tx_inclusion_list(state: BeaconState, block: BeaconBlock) -> None: - # TODO: cap gas usage, comunicate with the engine. +def process_tx_inclusion_list(state: BeaconState, block: BeaconBlock, execution_engine: ExecutionEngine) -> None: + inclusion_list = block.body.tx_inclusion_list + # Verify that the list is empty if the parent consensus block did not contain a payload + if state.current_signed_execution_payload_header.message != state.latest_execution_payload_header: + assert not inclusion_list + return + assert notify_new_inclusion_list(execution_engine, inclusion_list) state.previous_tx_inclusion_list = state.current_tx_inclusion_list - state.current_tx_inclusion_list = block.body.tx_inclusion_list + state.current_tx_inclusion_list = inclusion_list ``` From 0e2267d8f2ad1f49ade7fe8674fb5882bf8cb7de Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 8 Aug 2023 13:07:15 -0300 Subject: [PATCH 003/112] add small design notes for ILs --- specs/_features/epbs/design.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 specs/_features/epbs/design.md diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md new file mode 100644 index 0000000000..1cc3d86aa9 --- /dev/null +++ b/specs/_features/epbs/design.md @@ -0,0 +1,17 @@ +# ePBS design notes + +## Inclusion lists + +ePBS introduces forward inclusion lists for proposers to guarantee censor resistanship of the network. They are implemented as follows + +- Proposer for slot N submits a signed block that includes some transactions to be included at the beginning of slot N+1. +- Validators for slot N will consider the block invalid if those transactions are not executable at the start of slot N (this makes it impossible to put transactions that are only valid at slot N+1 for example, but still the proposer can protect from unbundling/replaying by binding the transaction to only be valid up to N+1 for example, because the block for N has already been committed by the builder). +- The IL is put in the `BeaconState`. +- The builder for slot N reveals its payload. This payload also contains a `beacon_state_root`. Validators for this slot will remove from the IL any transaction that was already executed during slot N (for example the builder may have independently included this transaction) or that became invalid because of other transactions from the same sender that appeared during N. They also check that the resulting `BeaconState` has the same `beacon_state_root` committed to by the builder. The upshot of this step is that the IL in the beacon state contains transactions that are guaranteed to be valid and executable during the beginning of N+1. +- The proposer for N+1 produces a block with its own IL for N+2. The builder for N+1 reveals its payload, and validators deem it invalid if the first transactions do not agree with the corresponding IL exactly. + +**Note:** in the event that the payload for the canonical block in slot N is not revealed, then the IL for slot N remains valid, the proposer for slot N+1 is not allowed to include a new IL. + +There are some concerns about proposers using IL for data availability, since the CL will have to keep the blocks somewhere to reconstruct the beacon state. There is a nice design by @vbuterin that instead of committing the IL to state, allows the txs to go on a sidecar together with a signed summary. The builder then needs to include the signed summary and a block that satisfies it. This design trades complexity for safety under the free DA issue of the above. + + From 844381d39f148585de9d865b855e490adca07899 Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 8 Aug 2023 13:26:12 -0300 Subject: [PATCH 004/112] check for gas usage after payload insertion Also notice that the DA issue only happens if all the transactions are invalidated in the same slot --- specs/_features/epbs/beacon-chain.md | 4 +++- specs/_features/epbs/design.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 7383485ddb..d1611f0c88 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -273,7 +273,9 @@ def verify_and_notify_new_payload(self: ExecutionEngine, Raise an exception if ``execution_payload`` is not valid with respect to ``self.execution_state``. Returns the list of transactions in the inclusion list that remain valid after executing the payload. That is it is guaranteed that the transactions returned in the list can be executed in the exact order starting from the - current ``self.execution_state``. + current ``self.execution_state``. This check also includes that the transactions still use less than + ``MAX_GAS_PER_INCLUSION_LIST``, since the gas usage may have been different if the transaction was + executed before or after slot N """ assert self.is_valid_block_hash(new_payload_request.execution_payload) return self.notify_new_payload(new_payload_request.execution_payload) diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index 1cc3d86aa9..a89c1dddef 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -12,6 +12,6 @@ ePBS introduces forward inclusion lists for proposers to guarantee censor resist **Note:** in the event that the payload for the canonical block in slot N is not revealed, then the IL for slot N remains valid, the proposer for slot N+1 is not allowed to include a new IL. -There are some concerns about proposers using IL for data availability, since the CL will have to keep the blocks somewhere to reconstruct the beacon state. There is a nice design by @vbuterin that instead of committing the IL to state, allows the txs to go on a sidecar together with a signed summary. The builder then needs to include the signed summary and a block that satisfies it. This design trades complexity for safety under the free DA issue of the above. +There are some concerns about proposers using IL for data availability, since the CL will have to keep the blocks somewhere to reconstruct the beacon state. A proposer may freely include a IL in a block by including transactions and invalidating them all in the payload for the same slot N. There is a nice design by @vbuterin that instead of committing the IL to state, allows the txs to go on a sidecar together with a signed summary. The builder then needs to include the signed summary and a block that satisfies it. This design trades complexity for safety under the free DA issue of the above. From ee37d4c043db785ba35bc0cbf5560175b399f312 Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 8 Aug 2023 15:05:04 -0300 Subject: [PATCH 005/112] Add helpers that change on ePBS --- specs/_features/epbs/beacon-chain.md | 172 ++++++++++++++++++++++++++- specs/_features/epbs/design.md | 7 ++ 2 files changed, 175 insertions(+), 4 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index d1611f0c88..ffcf96ee4d 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -24,6 +24,14 @@ At any given slot, the status of the blockchain's head may be either For a further introduction please refer to this [ethresear.ch article](https://ethresear.ch/t/payload-timeliness-committee-ptc-an-epbs-design/16054) +## Configuration + +### Time parameters + +| Name | Value | Unit | Duration | +| - | - | :-: | :-: | +| `SECONDS_PER_SLOT` | `uint64(16)` | seconds | 16 seconds # (Modified in ePBS) | + ## Preset ### Misc @@ -64,7 +72,7 @@ For a further introduction please refer to this [ethresear.ch article](https://e ## Containers -### New Containers +### New containers #### `Builder` @@ -73,6 +81,7 @@ class Builder(Container): pubkey: BLSPubkey withdrawal_address: ExecutionAddress # Commitment to pubkey for withdrawals effective_balance: Gwei # Balance at stake + slashed: boolean exit_epoch: Epoch withdrawable_epoch: Epoch # When builder can withdraw funds ``` @@ -101,7 +110,7 @@ class SignedExecutionPayloadEnvelope(Container): signature: BLSSignature ``` -### Modified Containers +### Modified containers #### `ExecutionPayload` @@ -226,6 +235,163 @@ class BeaconState(Container): current_tx_inclusion_list: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] # [New in ePBS] current_signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] ``` +## Helper functions + +### Predicates + +#### Modified `is_active_builder` + +```python +def is_active_builder(builder: Builder, epoch: Epoch) -> bool: + return epoch < builder.exit_epoch +``` +### Misc + +#### Modified `compute_proposer_index` +*Note*: `compute_proposer_index` is modified to account for builders being validators + +TODO: actually do the sampling proportional to effective balance + +### Beacon state accessors + +#### Modified `get_active_validator_indices` + +```python +def get_active_validator_indices(state: BeaconState, epoch: Epoch) -> Sequence[ValidatorIndex]: + """ + Return the sequence of active validator indices at ``epoch``. + """ + builder_indices = [ValidatorIndex(len(state.validators) + i) for i,b in enumerate(state.builders) if is_active_builder(b,epoch)] + return [ValidatorIndex(i) for i, v in enumerate(state.validators) if is_active_validator(v, epoch)] + builder_indices +``` + +#### New `get_effective_balance` + +```python +def get_effective_balance(state: BeaconState, index: ValidatorIndex) -> Gwei: + """ + Return the effective balance for the validator or the builder indexed by ``index`` + """ + if index < len(state.validators): + return state.validators[index].effective_balance + return state.builders[index-len(state.validators)].effective_balance +``` + +#### Modified `get_total_balance` + +```python +def get_total_balance(state: BeaconState, indices: Set[ValidatorIndex]) -> Gwei: + """ + Return the combined effective balance of the ``indices``. + ``EFFECTIVE_BALANCE_INCREMENT`` Gwei minimum to avoid divisions by zero. + Math safe up to ~10B ETH, after which this overflows uint64. + """ + return Gwei(max(EFFECTIVE_BALANCE_INCREMENT, sum([get_effective_balance(state, index) for index in indices]))) +``` + +### Beacon state mutators + +#### Modified `increase_balance` + +```python +def increase_balance(state: BeaconState, index: ValidatorIndex, delta: Gwei) -> None: + """ + Increase the validator balance at index ``index`` by ``delta``. + """ + if index < len(state.validators): + state.balances[index] += delta + return + state.builder_balances[index-len(state.validators)] += delta +``` + +#### Modified `decrease_balance` + +```python +def decrease_balance(state: BeaconState, index: ValidatorIndex, delta: Gwei) -> None: + """ + Decrease the validator balance at index ``index`` by ``delta``, with underflow protection. + """ + if index < len(state.validators) + state.balances[index] = 0 if delta > state.balances[index] else state.balances[index] - delta + return + index -= len(state.validators) + state.builder_balances[index] = 0 if delta > state.builder_balances[index] else state.builder_balances[index] - delta +``` + +#### Modified `initiate_validator_exit` + +```python +def initiate_validator_exit(state: BeaconState, index: ValidatorIndex) -> None: + """ + Initiate the exit of the validator with index ``index``. + """ + # Notice that the local variable ``validator`` may refer to a builder. Also that it continues defined outside + # its declaration scope. This is valid Python. + if index < len(state.validators): + validator = state.validators[index] + else: + validator = state.builders[index - len(state.validators)] + + # Return if validator already initiated exit + if validator.exit_epoch != FAR_FUTURE_EPOCH: + return + + # Compute exit queue epoch + exit_epochs = [v.exit_epoch for v in state.validators + state.builders if v.exit_epoch != FAR_FUTURE_EPOCH] + exit_queue_epoch = max(exit_epochs + [compute_activation_exit_epoch(get_current_epoch(state))]) + exit_queue_churn = len([v for v in state.validators + state.builders if v.exit_epoch == exit_queue_epoch]) + if exit_queue_churn >= get_validator_churn_limit(state): + exit_queue_epoch += Epoch(1) + + # Set validator exit epoch and withdrawable epoch + validator.exit_epoch = exit_queue_epoch + validator.withdrawable_epoch = Epoch(validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY) # TODO: Do we want to differentiate builders here? +``` + +#### New `proposer_slashing_amount` + +```python +def proposer_slashing_amount(slashed_index: ValidatorIndex, effective_balance: Gwei): + return min(MAX_EFFECTIVE_BALANCE, effective_balance) // MIN_SLASHING_PENALTY_QUOTIENT +``` + +#### Modified `slash_validator` + +```python +def slash_validator(state: BeaconState, + slashed_index: ValidatorIndex, + proposer_slashing: bool, + whistleblower_index: ValidatorIndex=None) -> None: + """ + Slash the validator with index ``slashed_index``. + """ + epoch = get_current_epoch(state) + initiate_validator_exit(state, slashed_index) + # Notice that the local variable ``validator`` may refer to a builder. Also that it continues defined outside + # its declaration scope. This is valid Python. + if index < len(state.validators): + validator = state.validators[slashed_index] + else: + validator = state.builders[slashed_index - len(state.validators)] + validator.slashed = True + validator.withdrawable_epoch = max(validator.withdrawable_epoch, Epoch(epoch + EPOCHS_PER_SLASHINGS_VECTOR)) + state.slashings[epoch % EPOCHS_PER_SLASHINGS_VECTOR] += validator.effective_balance + if proposer_slashing: + decrease_balance(state, slashed_index, proposer_slashing_amount(slashed_index, validator.effective_balance)) + else: + decrease_balance(state, slashed_index, validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT) + + # Apply proposer and whistleblower rewards + proposer_index = get_beacon_proposer_index(state) + if whistleblower_index is None: + whistleblower_index = proposer_index + whistleblower_reward = Gwei(max(MAX_EFFECTIVE_BALANCE, validator.effective_balance) // WHISTLEBLOWER_REWARD_QUOTIENT) + proposer_reward = Gwei(whistleblower_reward // PROPOSER_REWARD_QUOTIENT) + increase_balance(state, proposer_index, proposer_reward) + increase_balance(state, whistleblower_index, Gwei(whistleblower_reward - proposer_reward)) +``` + + ## Beacon chain state transition function @@ -362,8 +528,6 @@ def process_execution_payload_header(state: BeaconState, signed_header: SignedEx #### 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. -TODO: Deal with the case when the payload becomes invalid because of the forward inclusion list. - ```python def process_execution_payload(state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope, execution_engine: ExecutionEngine) -> None: # Verify signature [New in ePBS] diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index a89c1dddef..9b91edafc2 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -14,4 +14,11 @@ ePBS introduces forward inclusion lists for proposers to guarantee censor resist There are some concerns about proposers using IL for data availability, since the CL will have to keep the blocks somewhere to reconstruct the beacon state. A proposer may freely include a IL in a block by including transactions and invalidating them all in the payload for the same slot N. There is a nice design by @vbuterin that instead of committing the IL to state, allows the txs to go on a sidecar together with a signed summary. The builder then needs to include the signed summary and a block that satisfies it. This design trades complexity for safety under the free DA issue of the above. +## Builders +There is a new entity `Builder` that is a glorified validator required to have a higher stake and required to sign when producing execution payloads. + +- There is a new list in the `BeaconState` that contains all the registered builders +- Builders are also validators (otherwise their staked capital depreciates) +- We onboard builders by simply turning validators into builders if they achieve the necessary minimum balance (this way we avoid two forks to onboard builders and keep the same deposit flow, avoid builders to skip the entry churn) +- The unit `ValidatorIndex` is used for both indexing validators and builders, after all, builders are validators. Throughout the code, we often see checks of the form `index < len(state.validators)`, thus we consider a `ValidatorIndex(len(state.validators))` to correspond to the first builder, that is `state.builders[0]`. From 13d75bc90672d130b219559198c396cccfcf532b Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 9 Aug 2023 08:23:57 -0300 Subject: [PATCH 006/112] switch to gas limit instead of usage as pointed by V. Buterin --- specs/_features/epbs/beacon-chain.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index ffcf96ee4d..e93e80e8e9 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -392,7 +392,6 @@ def slash_validator(state: BeaconState, ``` - ## 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. @@ -439,9 +438,7 @@ def verify_and_notify_new_payload(self: ExecutionEngine, Raise an exception if ``execution_payload`` is not valid with respect to ``self.execution_state``. Returns the list of transactions in the inclusion list that remain valid after executing the payload. That is it is guaranteed that the transactions returned in the list can be executed in the exact order starting from the - current ``self.execution_state``. This check also includes that the transactions still use less than - ``MAX_GAS_PER_INCLUSION_LIST``, since the gas usage may have been different if the transaction was - executed before or after slot N + current ``self.execution_state``. """ assert self.is_valid_block_hash(new_payload_request.execution_payload) return self.notify_new_payload(new_payload_request.execution_payload) @@ -454,7 +451,7 @@ 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 current ``self.execution_state`` and that they consume less or equal than + starting from the current ``self.execution_state`` and that their total gas limit is less or equal than ```MAX_GAS_PER_INCLUSION_LIST``. """ ... From f4b566bc104d924980595b94ad1e93146871a573 Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 9 Aug 2023 11:27:55 -0300 Subject: [PATCH 007/112] Added more changed helpers Notice that the whole thing is broken by the construct if index < len(validators) else. --- specs/_features/epbs/beacon-chain.md | 219 ++++++++++++++++++++++++++- 1 file changed, 218 insertions(+), 1 deletion(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index e93e80e8e9..32d00895cf 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -231,6 +231,8 @@ class BeaconState(Container): # PBS builders: List[Builder, BUILDER_REGISTRY_LIMIT] # [New in ePBS] builder_balances: List[Gwei, BUILDER_REGISTRY_LIMIT] # [New in ePBS] + previous_epoch_builder_participation: List[ParticipationFlags, BUILDER_REGISTRY_LIMIT] # [New in ePBS] + current_epoch_builder_participation: List[ParticipationFlags, BUILDER_REGISTRY_LIMIT] # [New in ePBS] previous_tx_inclusion_list: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] # [New in ePBS] current_tx_inclusion_list: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] # [New in ePBS] current_signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] @@ -239,12 +241,51 @@ class BeaconState(Container): ### Predicates -#### Modified `is_active_builder` +#### New `is_active_builder` ```python def is_active_builder(builder: Builder, epoch: Epoch) -> bool: return epoch < builder.exit_epoch ``` + +#### New `is_slashable_builder` + +```python +def is_slashable_builder(builder: Builder, epoch: Epoch) -> bool: + """ + Check if ``builder`` is slashable. + """ + return (not validator.slashed) and (epoch < builder.withdrawable_epoch) +``` + +#### New `is_active_validator_at_index` + +```python +def is_active_validator_at_index(state: BeaconState, index: ValidatorIndex, epoch: Epoch) -> bool: + if index < len(state.validators): + return is_active_validator(state.validators[index], epoch) + return is_active_builder(state.builders[index-len(state.validators)], epoch) +``` + +#### Modified `is_valid_indexed_attestation` + +```python +def is_valid_indexed_attestation(state: BeaconState, indexed_attestation: IndexedAttestation) -> bool: + """ + Check if ``indexed_attestation`` is not empty, has sorted and unique indices and has a valid aggregate signature. + """ + # Verify indices are sorted and unique + indices = indexed_attestation.attesting_indices + if len(indices) == 0 or not indices == sorted(set(indices)): + return False + # Verify aggregate signature + pubkeys = [state.validators[i].pubkey if i < len(state.validators) else state.builders[i - len(state.validators)].pubkey for i in indices] + domain = get_domain(state, DOMAIN_BEACON_ATTESTER, indexed_attestation.data.target.epoch) + signing_root = compute_signing_root(indexed_attestation.data, domain) + return bls.FastAggregateVerify(pubkeys, signing_root, indexed_attestation.signature) +``` + + ### Misc #### Modified `compute_proposer_index` @@ -289,6 +330,96 @@ def get_total_balance(state: BeaconState, indices: Set[ValidatorIndex]) -> Gwei: return Gwei(max(EFFECTIVE_BALANCE_INCREMENT, sum([get_effective_balance(state, index) for index in indices]))) ``` +#### Modified `get_next_sync_committee_indices` + +*TODO*: make the shuffling actually weighted by the builder's effective balance + +```python +def get_next_sync_committee_indices(state: BeaconState) -> Sequence[ValidatorIndex]: + """ + Return the sync committee indices, with possible duplicates, for the next sync committee. + """ + epoch = Epoch(get_current_epoch(state) + 1) + + MAX_RANDOM_BYTE = 2**8 - 1 + active_validator_indices = get_active_validator_indices(state, epoch) + active_validator_count = uint64(len(active_validator_indices)) + seed = get_seed(state, epoch, DOMAIN_SYNC_COMMITTEE) + i = 0 + sync_committee_indices: List[ValidatorIndex] = [] + while len(sync_committee_indices) < SYNC_COMMITTEE_SIZE: + shuffled_index = compute_shuffled_index(uint64(i % active_validator_count), active_validator_count, seed) + candidate_index = active_validator_indices[shuffled_index] + random_byte = hash(seed + uint_to_bytes(uint64(i // 32)))[i % 32] + if candidate_index >= len(state.validators): + sync_commitee_indices.append(candidate_index) + else: + effective_balance = state.validators[candidate_index].effective_balance + if effective_balance * MAX_RANDOM_BYTE >= MAX_EFFECTIVE_BALANCE * random_byte: + sync_committee_indices.append(candidate_index) + i += 1 + return sync_committee_indices +``` + +#### Modified `get_next_sync_committee` + +```python +def get_next_sync_committee(state: BeaconState) -> SyncCommittee: + """ + Return the next sync committee, with possible pubkey duplicates. + """ + indices = get_next_sync_committee_indices(state) + pubkeys = [state.validators[index].pubkey if index < len(state.validators) else state.builders[index-len(state.validators)] for index in indices] + aggregate_pubkey = eth_aggregate_pubkeys(pubkeys) + return SyncCommittee(pubkeys=pubkeys, aggregate_pubkey=aggregate_pubkey) +``` + +#### Modified `get_unslashed_participating_indices` + +```python +def get_unslashed_participating_indices(state: BeaconState, flag_index: int, epoch: Epoch) -> Set[ValidatorIndex]: + """ + Return the set of validator indices that are both active and unslashed for the given ``flag_index`` and ``epoch``. + """ + assert epoch in (get_previous_epoch(state), get_current_epoch(state)) + if epoch == get_current_epoch(state): + epoch_participation = state.current_epoch_participation + epoch_builder_participation = state.current_epoch_builder_participation + else: + epoch_participation = state.previous_epoch_participation + epoch_builder_participation = state.previous_epoch_builder_participation + active_validator_indices = get_active_validator_indices(state, epoch) + participating_indices = [i for i in active_validator_indices if (has_flag(epoch_participation[i], flag_index) if i < len(state.validators) else has_flag(epoch_builder_participation[i-len(state.validators)], flag_index))] + return set(filter(lambda index: not state.validators[index].slashed if index < len(state.validators) else not state.builders[index-len(state.validators)].slashed, participating_indices)) +``` + +#### Modified `get_flag_index_deltas` + +```python +def get_flag_index_deltas(state: BeaconState, flag_index: int) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: + """ + Return the deltas for a given ``flag_index`` by scanning through the participation flags. + """ + rewards = [Gwei(0)] * (len(state.validators) + len(state.builders)) + penalties = [Gwei(0)] * (len(state.validators) + len(state.builders) + previous_epoch = get_previous_epoch(state) + unslashed_participating_indices = get_unslashed_participating_indices(state, flag_index, previous_epoch) + weight = PARTICIPATION_FLAG_WEIGHTS[flag_index] + unslashed_participating_balance = get_total_balance(state, unslashed_participating_indices) + unslashed_participating_increments = unslashed_participating_balance // EFFECTIVE_BALANCE_INCREMENT + active_increments = get_total_active_balance(state) // EFFECTIVE_BALANCE_INCREMENT + for index in get_eligible_validator_indices(state): + base_reward = get_base_reward(state, index) + if index in unslashed_participating_indices: + if not is_in_inactivity_leak(state): + reward_numerator = base_reward * weight * unslashed_participating_increments + rewards[index] += Gwei(reward_numerator // (active_increments * WEIGHT_DENOMINATOR)) + elif flag_index != TIMELY_HEAD_FLAG_INDEX: + penalties[index] += Gwei(base_reward * weight // WEIGHT_DENOMINATOR) + return rewards, penalties +``` + + ### Beacon state mutators #### Modified `increase_balance` @@ -400,6 +531,92 @@ The post-state corresponding to a pre-state `state` and a signed block `signed_b 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. +### Modified `verify_block_signature` + +```python +def verify_block_signature(state: BeaconState, signed_block: SignedBeaconBlock) -> bool: + index = signed_block.message.proposer_index + if index < len(state.validators): + proposer = state.validators[index] + else: + proposer = state.builders[index-len(state.validators)] + signing_root = compute_signing_root(signed_block.message, get_domain(state, DOMAIN_BEACON_PROPOSER)) + return bls.Verify(proposer.pubkey, signing_root, signed_block.signature) +``` + +### Epoch processing + +#### Modified `process_epoch` + +```python +def process_epoch(state: BeaconState) -> None: + process_justification_and_finalization(state) + process_inactivity_updates(state) + process_rewards_and_penalties(state) + process_registry_updates(state) + process_slashings(state) + process_eth1_data_reset(state) + process_effective_balance_updates(state) + process_slashings_reset(state) + process_randao_mixes_reset(state) + process_historical_summaries_update(state) + process_participation_flag_updates(state) # [Modified in ePBS] + process_sync_committee_updates(state) + process_builder_updates(state) # [New in ePBS] +``` + +#### Modified `process_participation_flag_updates` + +```python +def process_participation_flag_updates(state: BeaconState) -> None: + state.previous_epoch_participation = state.current_epoch_participation + state.current_epoch_participation = [ParticipationFlags(0b0000_0000) for _ in range(len(state.validators))] + state.previous_epoch_builder_participation = state.current_epoch_builder_participation + state.current_epoch_builder_participation = [ParticipationFlags(0b0000_0000) for _ in range(len(state.builders))] +``` + +#### Rewards and penalties + +##### Helpers + +*Note*: the function `get_base_reward` is modified to account for builders. + +```python +def get_base_reward(state: BeaconState, index: ValidatorIndex) -> Gwei: + """ + Return the base reward for the validator defined by ``index`` with respect to the current ``state``. + """ + if index < len(state.validators): + validator = state.validators[index] + else: + validator = state.builders[index-len(state.validators)] + increments = validator.effective_balance // EFFECTIVE_BALANCE_INCREMENT + return Gwei(increments * get_base_reward_per_increment(state)) +``` + +*Note*: The function `is_active_validator_at_index` is new + +```python +def is_active_validator_at_index(state: BeaconState, index: ValidatorIndex) -> Bool: + if index < len(state.validators): + validator = state.validators[index] + else: + validator = state + +``` + +*Note*: The function `get_eligible_validator_indices` is modified to account for builders. + +```python +def get_eligible_validator_indices(state: BeaconState) -> Sequence[ValidatorIndex]: + previous_epoch = get_previous_epoch(state) + return [ + ValidatorIndex(index) for index, v in enumerate(state.validators + state.builders) + if is_active_validator(v, previous_epoch) or (v.slashed and previous_epoch + 1 < v.withdrawable_epoch) + ] +``` + + ### Execution engine #### Request data From 08b881d12b4ae63433070221f4ef7b24ccb7cd5f Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 22 Aug 2023 13:18:51 -0300 Subject: [PATCH 008/112] change design notes --- specs/_features/epbs/design.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index 9b91edafc2..cd76fc8431 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -2,23 +2,22 @@ ## Inclusion lists -ePBS introduces forward inclusion lists for proposers to guarantee censor resistanship of the network. They are implemented as follows +ePBS introduces forward inclusion lists for proposers to guarantee censor resistanship of the network. We follow the design described in [this post](https://ethresear.ch/t/no-free-lunch-a-new-inclusion-list-design/16389). -- Proposer for slot N submits a signed block that includes some transactions to be included at the beginning of slot N+1. -- Validators for slot N will consider the block invalid if those transactions are not executable at the start of slot N (this makes it impossible to put transactions that are only valid at slot N+1 for example, but still the proposer can protect from unbundling/replaying by binding the transaction to only be valid up to N+1 for example, because the block for N has already been committed by the builder). -- The IL is put in the `BeaconState`. -- The builder for slot N reveals its payload. This payload also contains a `beacon_state_root`. Validators for this slot will remove from the IL any transaction that was already executed during slot N (for example the builder may have independently included this transaction) or that became invalid because of other transactions from the same sender that appeared during N. They also check that the resulting `BeaconState` has the same `beacon_state_root` committed to by the builder. The upshot of this step is that the IL in the beacon state contains transactions that are guaranteed to be valid and executable during the beginning of N+1. -- The proposer for N+1 produces a block with its own IL for N+2. The builder for N+1 reveals its payload, and validators deem it invalid if the first transactions do not agree with the corresponding IL exactly. +- Proposer for slot N submits a signed block and in parallel broadcasts pairs of `summaries` and `transactions` to be included at the beginning of slot N+1. `transactions` are just list of transactions that this proposer wants included at the most at the beginning of N+1. `Summaries` are lists consisting on addresses sending those transactions and their gas limits. The summaries are signed, the transactions aren't. An honest proposer is allowed to send many of these pairs that aren't committed to its beacon block so no double proposing slashing is involved. +- Validators for slot N will consider the block for validation only if they have seen at least one pair (summary, transactions). They will consider the block invalid if those transactions are not executable at the start of slot N and if they don't have at least 12.5% higher `maxFeePerGas` than the current slot's `maxFeePerGas`. +- The builder for slot N reveals its payload together with a signed summary of the proposer of slot N-1. The payload is considered only valid if the following applies + - Let k >= 0 be the minimum such that tx[0],...,tx[k-1], the first `k` transactions of the payload of slot N, satisfy some entry in the summary and `tx[k]` does not satisfy any entry. + - There exist transactions in the payload for N-1 that satisfy all the remaining entries in the summary. + - The payload is executable, that is, it's valid from the execution layer perspective. -**Note:** in the event that the payload for the canonical block in slot N is not revealed, then the IL for slot N remains valid, the proposer for slot N+1 is not allowed to include a new IL. +**Note:** in the event that the payload for the canonical block in slot N is not revealed, then the summaries and transactions list for slot N-1 remains valid, the honest proposer for slot N+1 is not allowed to submit a new IL and any such message will be ignored. The builder for N+1 still has to satisfy the summary of N-1. If there are k slots in a row that are missing payloads, the next full slot will still need to satisfy the inclusion list for N-1. -There are some concerns about proposers using IL for data availability, since the CL will have to keep the blocks somewhere to reconstruct the beacon state. A proposer may freely include a IL in a block by including transactions and invalidating them all in the payload for the same slot N. There is a nice design by @vbuterin that instead of committing the IL to state, allows the txs to go on a sidecar together with a signed summary. The builder then needs to include the signed summary and a block that satisfies it. This design trades complexity for safety under the free DA issue of the above. ## Builders There is a new entity `Builder` that is a glorified validator required to have a higher stake and required to sign when producing execution payloads. -- There is a new list in the `BeaconState` that contains all the registered builders -- Builders are also validators (otherwise their staked capital depreciates) -- We onboard builders by simply turning validators into builders if they achieve the necessary minimum balance (this way we avoid two forks to onboard builders and keep the same deposit flow, avoid builders to skip the entry churn) -- The unit `ValidatorIndex` is used for both indexing validators and builders, after all, builders are validators. Throughout the code, we often see checks of the form `index < len(state.validators)`, thus we consider a `ValidatorIndex(len(state.validators))` to correspond to the first builder, that is `state.builders[0]`. +- Builders are also validators (otherwise their staked capital depreciates). +- We onboard builders by simply turning validators into builders if they achieve the necessary minimum balance (this way we avoid two forks to onboard builders and keep the same deposit flow, avoid builders to skip the entry churn), we change their withdrawal prefix to be distinguished from normal validators. +- We need to include several changes from the [MaxEB PR](https://github.com/michaelneuder/consensus-specs/pull/3) in order to account with builders having an increased balance that would otherwise depreciate. From d339dfc200d72ec0a2a50202c2693f953cf98a9f Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 25 Aug 2023 08:12:20 -0300 Subject: [PATCH 009/112] minor changes --- specs/_features/epbs/beacon-chain.md | 112 +++++++++++++++------------ 1 file changed, 62 insertions(+), 50 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 32d00895cf..077c16ed7b 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -24,6 +24,15 @@ At any given slot, the status of the blockchain's head may be either For a further introduction please refer to this [ethresear.ch article](https://ethresear.ch/t/payload-timeliness-committee-ptc-an-epbs-design/16054) +## Constants + +### Withdrawal prefixes + +| Name | Value | +| - | - | +| `BUILDER_WITHDRAWAL_PREFIX` | `Bytes1('0x0b')` # (New in ePBS) | + + ## Configuration ### Time parameters @@ -44,19 +53,22 @@ For a further introduction please refer to this [ethresear.ch article](https://e | Name | Value | | - | - | -| `DOMAIN_BEACON_BUILDER` | `DomainType('0x0B000000')` | - -### State list lengths - -| Name | Value | Unit | Duration | -| - | - | :-: | :-: | -| `BUILDER_REGISTRY_LIMIT` | `uint64(2**20)` (=1,048,576) | builders | +| `DOMAIN_BEACON_BUILDER` | `DomainType('0x0B000000')` # (New in ePBS)| ### Gwei values | Name | Value | | - | - | -| `BUILDER_MIN_BALANCE` | `Gwei(2**10 * 10**9)` = (1,024,000,000,000) | +| `BUILDER_MIN_BALANCE` | `Gwei(2**10 * 10**9)` = (1,024,000,000,000) # (New in ePBS)| +| `MIN_ACTIVATION_BALANCE` | `Gwei(2**5 * 10**9)` (= 32,000,000,000) # (New in ePBS)| +| `EFFECTIVE_BALANCE_INCREMENT` | `Gwei(2**0 * 10**9)` (= 1,000,000,000) # (New in ePBS)| +| `MAX_EFFECTIVE_BALANCE` | `Gwei(2**11 * 10**9)` = (2,048,000,000,000) # (Modified in ePBS) | + +### Rewards and penalties + +| Name | Value | +| - | - | +| `PROPOSER_EQUIVOCATION_PENALTY_FACTOR` | `uint64(2**2)` (= 4) # (New in ePBS)| ### Incentivization weights @@ -68,24 +80,12 @@ For a further introduction please refer to this [ethresear.ch article](https://e | Name | Value | | - | - | | MAX_TRANSACTIONS_PER_INCLUSION_LIST | `2**4` (=16) | -| MAX_GAS_PER_INCLUSION_LIST | `2**20` (=1,048,576) | +| MAX_GAS_PER_INCLUSION_LIST | `2**21` (=2,097,152) | ## Containers ### New containers -#### `Builder` - -``` python -class Builder(Container): - pubkey: BLSPubkey - withdrawal_address: ExecutionAddress # Commitment to pubkey for withdrawals - effective_balance: Gwei # Balance at stake - slashed: boolean - exit_epoch: Epoch - withdrawable_epoch: Epoch # When builder can withdraw funds -``` - #### `SignedExecutionPayloadHeader` ```python @@ -99,6 +99,8 @@ class SignedExecutionPayloadHeader(Container): ```python class ExecutionPayloadEnvelope(Container): payload: ExecutionPayload + builder_index: ValidatorIndex + value: Gwei state_root: Root ``` @@ -110,10 +112,44 @@ class SignedExecutionPayloadEnvelope(Container): signature: BLSSignature ``` +#### `InclusionListSummaryEntry` + +```python +class InclusionListSummaryEntry(Container): + address: ExecutionAddress + gas_limit: uint64 +``` + +#### `InclusionListSummary` + +```python +class InclusionListSummary(Container) + proposer_index: ValidatorIndex + summary: List[InclusionListSummaryEntry, MAX_TRANSACTIONS_PER_INCLUSION_LIST] +``` + +#### `SignedInclusionListSummary` + +```python +class SignedInclusionListSummary(Container): + message: InclusionListSummary + signature: BLSSignature +``` + +#### `InclusionList` + +```python +class InclusionList(Container) + summary: SignedInclusionListSummary + transactions: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] +``` + ### Modified containers #### `ExecutionPayload` +**Note:** The `ExecutionPayload` is modified to contain the builder's index and the bid value. It also contains a transaction inclusion list summary signed by the corresponding beacon block proposer and the list of indices of transactions in the parent block that have to be excluded from the inclusion list summary because they were satisfied in the previous slot. + ```python class ExecutionPayload(Container): # Execution block header fields @@ -133,12 +169,14 @@ class ExecutionPayload(Container): block_hash: Hash32 # Hash of execution block transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD] withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] - builder_index: uint64 # [New in ePBS] - value: Gwei # [New in ePBS] + inclusion_list_summary: SignedInclusionListSummary # [New in ePBS] + inclusion_list_exclusions: List[uint64, MAX_TRANSACTIONS_PER_INCLUSION_LIST] # [New in ePBS] ``` #### `ExecutionPayloadHeader` +**Note:** The `ExecutionPayloadHeader` is modified to include the builder's index and the bid's value. + ```python class ExecutionPayloadHeader(Container): # Execution block header fields @@ -162,28 +200,8 @@ class ExecutionPayloadHeader(Container): value: Gwei # [New in ePBS] ``` -#### `BeaconBlockBody` - -```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_payload_header: SignedExecutionPayloadHeader # [Modified in ePBS] - bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] - tx_inclusion_list: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] -``` - - #### `BeaconState` -*Note*: the beacon state is modified to store a signed latest execution payload header and it adds a registry of builders, their balances and two transaction inclusion lists. +*Note*: the beacon state is modified to store a signed latest execution payload header. ```python class BeaconState(Container): @@ -229,12 +247,6 @@ class BeaconState(Container): # Deep history valid from Capella onwards historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] # PBS - builders: List[Builder, BUILDER_REGISTRY_LIMIT] # [New in ePBS] - builder_balances: List[Gwei, BUILDER_REGISTRY_LIMIT] # [New in ePBS] - previous_epoch_builder_participation: List[ParticipationFlags, BUILDER_REGISTRY_LIMIT] # [New in ePBS] - current_epoch_builder_participation: List[ParticipationFlags, BUILDER_REGISTRY_LIMIT] # [New in ePBS] - previous_tx_inclusion_list: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] # [New in ePBS] - current_tx_inclusion_list: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] # [New in ePBS] current_signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] ``` ## Helper functions From ff1eff7f728d2907712668875fcf9e7b99fe21a1 Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 25 Aug 2023 15:21:19 -0300 Subject: [PATCH 010/112] First forkchoice changes. - Added handlers for execution payloads and checks inclusion lists availability on consensus blocks --- specs/_features/epbs/beacon-chain.md | 195 +++++++++------------------ specs/_features/epbs/fork-choice.md | 173 ++++++++++++++++++++++++ 2 files changed, 235 insertions(+), 133 deletions(-) create mode 100644 specs/_features/epbs/fork-choice.md diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 077c16ed7b..ebe75c1691 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -64,6 +64,12 @@ For a further introduction please refer to this [ethresear.ch article](https://e | `EFFECTIVE_BALANCE_INCREMENT` | `Gwei(2**0 * 10**9)` (= 1,000,000,000) # (New in ePBS)| | `MAX_EFFECTIVE_BALANCE` | `Gwei(2**11 * 10**9)` = (2,048,000,000,000) # (Modified in ePBS) | +### Time parameters + +| Name | Value | Unit | Duration | +| - | - | :-: | :-: | +| `MIN_SLOTS_FOR_INCLUSION_LISTS_REQUESTS` | `uint64(2)` | slots | 32 seconds # (New in ePBS) | + ### Rewards and penalties | Name | Value | @@ -99,8 +105,7 @@ class SignedExecutionPayloadHeader(Container): ```python class ExecutionPayloadEnvelope(Container): payload: ExecutionPayload - builder_index: ValidatorIndex - value: Gwei + beacon_block_root: Root state_root: Root ``` @@ -146,6 +151,28 @@ class InclusionList(Container) ### 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] + signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] + bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] + blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] # [New in Deneb:EIP4844] +``` + #### `ExecutionPayload` **Note:** The `ExecutionPayload` is modified to contain the builder's index and the bid value. It also contains a transaction inclusion list summary signed by the corresponding beacon block proposer and the list of indices of transactions in the parent block that have to be excluded from the inclusion list summary because they were satisfied in the previous slot. @@ -169,6 +196,8 @@ class ExecutionPayload(Container): block_hash: Hash32 # Hash of execution block transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD] withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + builder_index: ValidatorIndex # [New in ePBS] + value: Gwei # [New in ePBS] inclusion_list_summary: SignedInclusionListSummary # [New in ePBS] inclusion_list_exclusions: List[uint64, MAX_TRANSACTIONS_PER_INCLUSION_LIST] # [New in ePBS] ``` @@ -196,8 +225,10 @@ class ExecutionPayloadHeader(Container): block_hash: Hash32 # Hash of execution block transactions_root: Root withdrawals_root: Root - builder_index: uint64 # [New in ePBS] + builder_index: ValidatorIndex # [New in ePBS] value: Gwei # [New in ePBS] + inclusion_list_summary_root: Root # [New in ePBS] + inclusion_list_exclusions_root: Root # [New in ePBS] ``` #### `BeaconState` @@ -572,63 +603,11 @@ def process_epoch(state: BeaconState) -> None: process_slashings_reset(state) process_randao_mixes_reset(state) process_historical_summaries_update(state) - process_participation_flag_updates(state) # [Modified in ePBS] + process_participation_flag_updates(state) process_sync_committee_updates(state) process_builder_updates(state) # [New in ePBS] ``` -#### Modified `process_participation_flag_updates` - -```python -def process_participation_flag_updates(state: BeaconState) -> None: - state.previous_epoch_participation = state.current_epoch_participation - state.current_epoch_participation = [ParticipationFlags(0b0000_0000) for _ in range(len(state.validators))] - state.previous_epoch_builder_participation = state.current_epoch_builder_participation - state.current_epoch_builder_participation = [ParticipationFlags(0b0000_0000) for _ in range(len(state.builders))] -``` - -#### Rewards and penalties - -##### Helpers - -*Note*: the function `get_base_reward` is modified to account for builders. - -```python -def get_base_reward(state: BeaconState, index: ValidatorIndex) -> Gwei: - """ - Return the base reward for the validator defined by ``index`` with respect to the current ``state``. - """ - if index < len(state.validators): - validator = state.validators[index] - else: - validator = state.builders[index-len(state.validators)] - increments = validator.effective_balance // EFFECTIVE_BALANCE_INCREMENT - return Gwei(increments * get_base_reward_per_increment(state)) -``` - -*Note*: The function `is_active_validator_at_index` is new - -```python -def is_active_validator_at_index(state: BeaconState, index: ValidatorIndex) -> Bool: - if index < len(state.validators): - validator = state.validators[index] - else: - validator = state - -``` - -*Note*: The function `get_eligible_validator_indices` is modified to account for builders. - -```python -def get_eligible_validator_indices(state: BeaconState) -> Sequence[ValidatorIndex]: - previous_epoch = get_previous_epoch(state) - return [ - ValidatorIndex(index) for index, v in enumerate(state.validators + state.builders) - if is_active_validator(v, previous_epoch) or (v.slashed and previous_epoch + 1 < v.withdrawable_epoch) - ] -``` - - ### Execution engine #### Request data @@ -638,41 +617,11 @@ def get_eligible_validator_indices(state: BeaconState) -> Sequence[ValidatorInde ```python @dataclass class NewInclusionListRequest(object): - inclusion_list: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] + inclusion_list: InclusionList ``` #### Engine APIs -#### Modified `notify_new_payload` -*Note*: the function notify new payload is modified to raise an exception if the payload is not valid, and to return the list of transactions that remain valid in the inclusion list - -```python -def notify_new_payload(self: ExecutionEngine, execution_payload: ExecutionPayload) -> List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST]: - """ - Raise an exception if ``execution_payload`` is not valid with respect to ``self.execution_state``. - Returns the list of transactions in the inclusion list that remain valid after executing the payload. That is - it is guaranteed that the transactions returned in the list can be executed in the exact order starting from the - current ``self.execution_state``. - """ - ... -``` - -#### Modified `verify_and_notify_new_payload` -*Note*: the function `verify_and_notify_new_payload` is modified so that it returns the list of transactions that remain valid in the forward inclusion list. It raises an exception if the payload is not valid. - -```python -def verify_and_notify_new_payload(self: ExecutionEngine, - new_payload_request: NewPayloadRequest) -> List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST]: - """ - Raise an exception if ``execution_payload`` is not valid with respect to ``self.execution_state``. - Returns the list of transactions in the inclusion list that remain valid after executing the payload. That is - it is guaranteed that the transactions returned in the list can be executed in the exact order starting from the - current ``self.execution_state``. - """ - assert self.is_valid_block_hash(new_payload_request.execution_payload) - return self.notify_new_payload(new_payload_request.execution_payload) -``` - #### New `notify_new_inclusion_list` ```python @@ -680,8 +629,8 @@ 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 current ``self.execution_state`` and that their total gas limit is less or equal than - ```MAX_GAS_PER_INCLUSION_LIST``. + starting from the current ``self.execution_state``, their 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 """ ... ``` @@ -690,13 +639,10 @@ def notify_new_inclusion_list(self: ExecutionEngine, *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 `process_tx_inclusion_list` needs to be processed before the payload header since the former requires to check the last committed payload header. - ```python def process_block(state: BeaconState, block: BeaconBlock) -> None: process_block_header(state, block) - process_tx_inclusion_list(state, block, EXECUTION_ENGINE) # [New in ePBS] process_execution_payload_header(state, block.body.execution_payload_header) # [Modified in ePBS] # Removed process_withdrawal in ePBS is processed during payload processing [Modified in ePBS] process_randao(state, block.body) @@ -705,34 +651,14 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None: process_sync_aggregate(state, block.body.sync_aggregate) ``` -#### New `update_tx_inclusion_lists` - -```python -def update_tx_inclusion_lists(state: BeaconState, payload: ExecutionPayload, engine: ExecutionEngine, inclusion_list: List[Transaction, MAX_TRANSACTION_PER_INCLUSION_LIST]) -> None: - old_transactions = payload.transactions[:len(state.previous_tx_inclusion_list)] - assert state.previous_tx_inclusion_list == old_transactions - - state.previous_tx_inclusion_list = inclusion_list -``` - #### New `verify_execution_payload_header_signature` ```python def verify_execution_payload_header_signature(state: BeaconState, signed_header: SignedExecutionPayloadHeader) -> bool: - builder = state.builders[signed_header.message.builder_index] + # 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)) - if not bls.Verify(builder.pubkey, signing_root, signed_header.signature): - return False - return -``` - -#### New `verify_execution_payload_signature` - -```python -def verify_execution_envelope_signature(state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope) -> bool: - builder = state.builders[signed_envelope.message.payload.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) + return bls.Verify(builder.pubkey, signing_root, signed_header.signature) ``` #### New `process_execution_payload_header` @@ -740,7 +666,11 @@ def verify_execution_envelope_signature(state: BeaconState, signed_envelope: Sig ```python def process_execution_payload_header(state: BeaconState, signed_header: SignedExecutionPayloadHeader) -> None: assert verify_execution_payload_header_signature(state, signed_header) + # Check that the builder has funds to cover the bid header = signed_header.message + builder_index = header.builder_index + if state.balances[builder_index] < header.value: + return false # Verify consistency of the parent hash with respect to the previous execution payload header assert header.parent_hash == state.latest_execution_payload_header.block_hash # Verify prev_randao @@ -751,6 +681,15 @@ def process_execution_payload_header(state: BeaconState, signed_header: SignedEx state.current_signed_execution_payload_header = signed_header ``` +#### New `verify_execution_payload_signature` + +```python +def verify_execution_envelope_signature(state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope) -> bool: + builder = state.validators[signed_envelope.message.payload.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) +``` + #### 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. @@ -764,9 +703,14 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti previous_hash = hash_tree_root(state.current_signed_execution_payload_header.message) assert hash == previous_hash # Verify the execution payload is valid - inclusion_list = execution_engine.verify_and_notify_new_payload(NewPayloadRequest(execution_payload=payload)) - # Verify and update the proposers inclusion lists - update_tx_inclusion_lists(state, payload, inclusion_list) + versioned_hashes = [kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments] + assert execution_engine.verify_and_notify_new_payload( + NewPayloadRequest( + execution_payload=payload, + versioned_hashes=versioned_hashes, + parent_beacon_block_root=state.latest_block_header.parent_root, + ) + ) # Process Withdrawals in the payload process_withdrawals(state, payload) # Cache the execution payload header @@ -774,18 +718,3 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti # Verify the state root assert signed_envelope.message.state_root == hash_tree_root(state) ``` - -#### New `process_tx_inclusion_list` - -```python -def process_tx_inclusion_list(state: BeaconState, block: BeaconBlock, execution_engine: ExecutionEngine) -> None: - inclusion_list = block.body.tx_inclusion_list - # Verify that the list is empty if the parent consensus block did not contain a payload - if state.current_signed_execution_payload_header.message != state.latest_execution_payload_header: - assert not inclusion_list - return - assert notify_new_inclusion_list(execution_engine, inclusion_list) - state.previous_tx_inclusion_list = state.current_tx_inclusion_list - state.current_tx_inclusion_list = inclusion_list -``` - diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md new file mode 100644 index 0000000000..6b20ebfec9 --- /dev/null +++ b/specs/_features/epbs/fork-choice.md @@ -0,0 +1,173 @@ +# ePBS -- Fork Choice + +## Table of contents + + + + + + + +## Introduction + +This is the modification of the fork choice accompanying the ePBS upgrade. + +## Helpers + +### `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 + + # 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) + + # TODO: These checks will also be performed by the EL surely so we can probably remove them from here. + # Check that the total gas limit is bounded + total_gas_limit = sum( entry.gas_limit for entry in summary) + assert total_gas_limit <= MAX_GAS_PER_INCLUSION_LIST + + # Check that the signature is correct + # TODO: do we need a new domain? + signing_root = compute_signing_root(signed_summary.message, get_domain(state, DOMAIN_BEACON_PROPOSER)) + proposer = state.validators[proposer_index] + assert bls.Verify(proposer.pubkey, signing_root, signed_summary.signature) + + # Check that the inclusion list is valid + return execution_engine.notify_new_inclusion_list(inclusion_list) +``` + +### `is_inclusion_list_available` +*[New in ePBS]* + +```python +def is_inclusion_list_available(state: BeaconState, block: BeaconBlock) -> bool: + """ + Returns whether one inclusion list for the corresponding block was seen in full and has been validated. + There is one exception if the parent consensus block did not contain an exceution payload, in which case + We return true early + + `retrieve_inclusion_list` is implementation and context dependent + It returns one inclusion list that was broadcasted during the given slot by the given proposer. + Note: the p2p network does not guarantee sidecar retrieval outside of + `MIN_SLOTS_FOR_INCLUSION_LISTS_REQUESTS` + """ + # Verify that the list is empty if the parent consensus block did not contain a payload + if state.current_signed_execution_payload_header.message != state.latest_execution_payload_header: + return true + + # verify the inclusion list + inclusion_list = retrieve_inclusion_list(block.slot, block.proposer_index) + return verify_inclusion_list(state, block, inclusion_list, EXECUTION_ENGINE) +``` + + +## 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. There is also the addition of the inclusion list availability check. + +```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] + parent_signed_payload_header = parent_block.body.signed_execution_payload_header + parent_payload_hash = paernt_signed_payload_header.message.block_hash + current_signed_payload_header = block.body.signed_execution_payload_header + current_payload_parent_hash = current_signed_payload_header.message.parent_hash + # Make a copy of the state to avoid mutability issues + if current_payload_parent_hash == parent_payload_hash: + assert block.parent_root in store.execution_payload_states + state = copy(store.execution_payload_states[block.parent_root]) + else: + 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 if blob data is available + # If not, this block MAY be queued and subsequently considered when blob data becomes available + assert is_data_available(hash_tree_root(block), block.body.blob_kzg_commitments) + + # Check if there is a valid inclusion list. + # This check is performed only if the block's slot is within the visibility window + # If not, this block MAY be queued and subsequently considered when a valid inclusion list becomes available + if block.slot + MIN_SLOTS_FOR_INCLUSION_LISTS_REQUESTS >= current_slot: + assert is_inclusion_list_available(state, 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 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_excecution_payload(store: Store, signed_envelope_: SignedExecutionPayloadEnvelope) -> None: + """ + Run ``on_execution_payload`` upon receiving a new execution payload. + """ + beacon_block_root = signed_envelope.beacon_block_root + # The corresponding beacon block root needs to be known + assert beacon_block_root in store.block_states + + # Make a copy of the state to avoid mutability issues + state = copy(store.block_states[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[beacon_block_root] = state +``` + From ef7bd408cd8775afdde4be9a6bcf3b761bd01b9e Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 25 Aug 2023 17:01:02 -0300 Subject: [PATCH 011/112] don't modify verify_block_signature --- specs/_features/epbs/beacon-chain.md | 13 ------------- specs/_features/epbs/fork-choice.md | 3 +-- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index ebe75c1691..1b217afc4b 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -574,19 +574,6 @@ The post-state corresponding to a pre-state `state` and a signed block `signed_b 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. -### Modified `verify_block_signature` - -```python -def verify_block_signature(state: BeaconState, signed_block: SignedBeaconBlock) -> bool: - index = signed_block.message.proposer_index - if index < len(state.validators): - proposer = state.validators[index] - else: - proposer = state.builders[index-len(state.validators)] - signing_root = compute_signing_root(signed_block.message, get_domain(state, DOMAIN_BEACON_PROPOSER)) - return bls.Verify(proposer.pubkey, signing_root, signed_block.signature) -``` - ### Epoch processing #### Modified `process_epoch` diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 6b20ebfec9..91774a6b2a 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -169,5 +169,4 @@ def on_excecution_payload(store: Store, signed_envelope_: SignedExecutionPayload #Add new state for this payload to the store store.execution_payload_states[beacon_block_root] = state -``` - +``` From 9dbd2c58b5ac3313f4841dd97a7c863bf6e5dc9e Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 25 Aug 2023 17:03:27 -0300 Subject: [PATCH 012/112] remove clutter --- specs/_features/epbs/beacon-chain.md | 287 +-------------------------- 1 file changed, 1 insertion(+), 286 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 1b217afc4b..802bc926c5 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -170,7 +170,7 @@ class BeaconBlockBody(Container): # Removed execution_payload [ Removed in ePBS] signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] - blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] # [New in Deneb:EIP4844] + blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] ``` #### `ExecutionPayload` @@ -280,291 +280,6 @@ class BeaconState(Container): # PBS current_signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] ``` -## Helper functions - -### Predicates - -#### New `is_active_builder` - -```python -def is_active_builder(builder: Builder, epoch: Epoch) -> bool: - return epoch < builder.exit_epoch -``` - -#### New `is_slashable_builder` - -```python -def is_slashable_builder(builder: Builder, epoch: Epoch) -> bool: - """ - Check if ``builder`` is slashable. - """ - return (not validator.slashed) and (epoch < builder.withdrawable_epoch) -``` - -#### New `is_active_validator_at_index` - -```python -def is_active_validator_at_index(state: BeaconState, index: ValidatorIndex, epoch: Epoch) -> bool: - if index < len(state.validators): - return is_active_validator(state.validators[index], epoch) - return is_active_builder(state.builders[index-len(state.validators)], epoch) -``` - -#### Modified `is_valid_indexed_attestation` - -```python -def is_valid_indexed_attestation(state: BeaconState, indexed_attestation: IndexedAttestation) -> bool: - """ - Check if ``indexed_attestation`` is not empty, has sorted and unique indices and has a valid aggregate signature. - """ - # Verify indices are sorted and unique - indices = indexed_attestation.attesting_indices - if len(indices) == 0 or not indices == sorted(set(indices)): - return False - # Verify aggregate signature - pubkeys = [state.validators[i].pubkey if i < len(state.validators) else state.builders[i - len(state.validators)].pubkey for i in indices] - domain = get_domain(state, DOMAIN_BEACON_ATTESTER, indexed_attestation.data.target.epoch) - signing_root = compute_signing_root(indexed_attestation.data, domain) - return bls.FastAggregateVerify(pubkeys, signing_root, indexed_attestation.signature) -``` - - -### Misc - -#### Modified `compute_proposer_index` -*Note*: `compute_proposer_index` is modified to account for builders being validators - -TODO: actually do the sampling proportional to effective balance - -### Beacon state accessors - -#### Modified `get_active_validator_indices` - -```python -def get_active_validator_indices(state: BeaconState, epoch: Epoch) -> Sequence[ValidatorIndex]: - """ - Return the sequence of active validator indices at ``epoch``. - """ - builder_indices = [ValidatorIndex(len(state.validators) + i) for i,b in enumerate(state.builders) if is_active_builder(b,epoch)] - return [ValidatorIndex(i) for i, v in enumerate(state.validators) if is_active_validator(v, epoch)] + builder_indices -``` - -#### New `get_effective_balance` - -```python -def get_effective_balance(state: BeaconState, index: ValidatorIndex) -> Gwei: - """ - Return the effective balance for the validator or the builder indexed by ``index`` - """ - if index < len(state.validators): - return state.validators[index].effective_balance - return state.builders[index-len(state.validators)].effective_balance -``` - -#### Modified `get_total_balance` - -```python -def get_total_balance(state: BeaconState, indices: Set[ValidatorIndex]) -> Gwei: - """ - Return the combined effective balance of the ``indices``. - ``EFFECTIVE_BALANCE_INCREMENT`` Gwei minimum to avoid divisions by zero. - Math safe up to ~10B ETH, after which this overflows uint64. - """ - return Gwei(max(EFFECTIVE_BALANCE_INCREMENT, sum([get_effective_balance(state, index) for index in indices]))) -``` - -#### Modified `get_next_sync_committee_indices` - -*TODO*: make the shuffling actually weighted by the builder's effective balance - -```python -def get_next_sync_committee_indices(state: BeaconState) -> Sequence[ValidatorIndex]: - """ - Return the sync committee indices, with possible duplicates, for the next sync committee. - """ - epoch = Epoch(get_current_epoch(state) + 1) - - MAX_RANDOM_BYTE = 2**8 - 1 - active_validator_indices = get_active_validator_indices(state, epoch) - active_validator_count = uint64(len(active_validator_indices)) - seed = get_seed(state, epoch, DOMAIN_SYNC_COMMITTEE) - i = 0 - sync_committee_indices: List[ValidatorIndex] = [] - while len(sync_committee_indices) < SYNC_COMMITTEE_SIZE: - shuffled_index = compute_shuffled_index(uint64(i % active_validator_count), active_validator_count, seed) - candidate_index = active_validator_indices[shuffled_index] - random_byte = hash(seed + uint_to_bytes(uint64(i // 32)))[i % 32] - if candidate_index >= len(state.validators): - sync_commitee_indices.append(candidate_index) - else: - effective_balance = state.validators[candidate_index].effective_balance - if effective_balance * MAX_RANDOM_BYTE >= MAX_EFFECTIVE_BALANCE * random_byte: - sync_committee_indices.append(candidate_index) - i += 1 - return sync_committee_indices -``` - -#### Modified `get_next_sync_committee` - -```python -def get_next_sync_committee(state: BeaconState) -> SyncCommittee: - """ - Return the next sync committee, with possible pubkey duplicates. - """ - indices = get_next_sync_committee_indices(state) - pubkeys = [state.validators[index].pubkey if index < len(state.validators) else state.builders[index-len(state.validators)] for index in indices] - aggregate_pubkey = eth_aggregate_pubkeys(pubkeys) - return SyncCommittee(pubkeys=pubkeys, aggregate_pubkey=aggregate_pubkey) -``` - -#### Modified `get_unslashed_participating_indices` - -```python -def get_unslashed_participating_indices(state: BeaconState, flag_index: int, epoch: Epoch) -> Set[ValidatorIndex]: - """ - Return the set of validator indices that are both active and unslashed for the given ``flag_index`` and ``epoch``. - """ - assert epoch in (get_previous_epoch(state), get_current_epoch(state)) - if epoch == get_current_epoch(state): - epoch_participation = state.current_epoch_participation - epoch_builder_participation = state.current_epoch_builder_participation - else: - epoch_participation = state.previous_epoch_participation - epoch_builder_participation = state.previous_epoch_builder_participation - active_validator_indices = get_active_validator_indices(state, epoch) - participating_indices = [i for i in active_validator_indices if (has_flag(epoch_participation[i], flag_index) if i < len(state.validators) else has_flag(epoch_builder_participation[i-len(state.validators)], flag_index))] - return set(filter(lambda index: not state.validators[index].slashed if index < len(state.validators) else not state.builders[index-len(state.validators)].slashed, participating_indices)) -``` - -#### Modified `get_flag_index_deltas` - -```python -def get_flag_index_deltas(state: BeaconState, flag_index: int) -> Tuple[Sequence[Gwei], Sequence[Gwei]]: - """ - Return the deltas for a given ``flag_index`` by scanning through the participation flags. - """ - rewards = [Gwei(0)] * (len(state.validators) + len(state.builders)) - penalties = [Gwei(0)] * (len(state.validators) + len(state.builders) - previous_epoch = get_previous_epoch(state) - unslashed_participating_indices = get_unslashed_participating_indices(state, flag_index, previous_epoch) - weight = PARTICIPATION_FLAG_WEIGHTS[flag_index] - unslashed_participating_balance = get_total_balance(state, unslashed_participating_indices) - unslashed_participating_increments = unslashed_participating_balance // EFFECTIVE_BALANCE_INCREMENT - active_increments = get_total_active_balance(state) // EFFECTIVE_BALANCE_INCREMENT - for index in get_eligible_validator_indices(state): - base_reward = get_base_reward(state, index) - if index in unslashed_participating_indices: - if not is_in_inactivity_leak(state): - reward_numerator = base_reward * weight * unslashed_participating_increments - rewards[index] += Gwei(reward_numerator // (active_increments * WEIGHT_DENOMINATOR)) - elif flag_index != TIMELY_HEAD_FLAG_INDEX: - penalties[index] += Gwei(base_reward * weight // WEIGHT_DENOMINATOR) - return rewards, penalties -``` - - -### Beacon state mutators - -#### Modified `increase_balance` - -```python -def increase_balance(state: BeaconState, index: ValidatorIndex, delta: Gwei) -> None: - """ - Increase the validator balance at index ``index`` by ``delta``. - """ - if index < len(state.validators): - state.balances[index] += delta - return - state.builder_balances[index-len(state.validators)] += delta -``` - -#### Modified `decrease_balance` - -```python -def decrease_balance(state: BeaconState, index: ValidatorIndex, delta: Gwei) -> None: - """ - Decrease the validator balance at index ``index`` by ``delta``, with underflow protection. - """ - if index < len(state.validators) - state.balances[index] = 0 if delta > state.balances[index] else state.balances[index] - delta - return - index -= len(state.validators) - state.builder_balances[index] = 0 if delta > state.builder_balances[index] else state.builder_balances[index] - delta -``` - -#### Modified `initiate_validator_exit` - -```python -def initiate_validator_exit(state: BeaconState, index: ValidatorIndex) -> None: - """ - Initiate the exit of the validator with index ``index``. - """ - # Notice that the local variable ``validator`` may refer to a builder. Also that it continues defined outside - # its declaration scope. This is valid Python. - if index < len(state.validators): - validator = state.validators[index] - else: - validator = state.builders[index - len(state.validators)] - - # Return if validator already initiated exit - if validator.exit_epoch != FAR_FUTURE_EPOCH: - return - - # Compute exit queue epoch - exit_epochs = [v.exit_epoch for v in state.validators + state.builders if v.exit_epoch != FAR_FUTURE_EPOCH] - exit_queue_epoch = max(exit_epochs + [compute_activation_exit_epoch(get_current_epoch(state))]) - exit_queue_churn = len([v for v in state.validators + state.builders if v.exit_epoch == exit_queue_epoch]) - if exit_queue_churn >= get_validator_churn_limit(state): - exit_queue_epoch += Epoch(1) - - # Set validator exit epoch and withdrawable epoch - validator.exit_epoch = exit_queue_epoch - validator.withdrawable_epoch = Epoch(validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY) # TODO: Do we want to differentiate builders here? -``` - -#### New `proposer_slashing_amount` - -```python -def proposer_slashing_amount(slashed_index: ValidatorIndex, effective_balance: Gwei): - return min(MAX_EFFECTIVE_BALANCE, effective_balance) // MIN_SLASHING_PENALTY_QUOTIENT -``` - -#### Modified `slash_validator` - -```python -def slash_validator(state: BeaconState, - slashed_index: ValidatorIndex, - proposer_slashing: bool, - whistleblower_index: ValidatorIndex=None) -> None: - """ - Slash the validator with index ``slashed_index``. - """ - epoch = get_current_epoch(state) - initiate_validator_exit(state, slashed_index) - # Notice that the local variable ``validator`` may refer to a builder. Also that it continues defined outside - # its declaration scope. This is valid Python. - if index < len(state.validators): - validator = state.validators[slashed_index] - else: - validator = state.builders[slashed_index - len(state.validators)] - validator.slashed = True - validator.withdrawable_epoch = max(validator.withdrawable_epoch, Epoch(epoch + EPOCHS_PER_SLASHINGS_VECTOR)) - state.slashings[epoch % EPOCHS_PER_SLASHINGS_VECTOR] += validator.effective_balance - if proposer_slashing: - decrease_balance(state, slashed_index, proposer_slashing_amount(slashed_index, validator.effective_balance)) - else: - decrease_balance(state, slashed_index, validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT) - - # Apply proposer and whistleblower rewards - proposer_index = get_beacon_proposer_index(state) - if whistleblower_index is None: - whistleblower_index = proposer_index - whistleblower_reward = Gwei(max(MAX_EFFECTIVE_BALANCE, validator.effective_balance) // WHISTLEBLOWER_REWARD_QUOTIENT) - proposer_reward = Gwei(whistleblower_reward // PROPOSER_REWARD_QUOTIENT) - increase_balance(state, proposer_index, proposer_reward) - increase_balance(state, whistleblower_index, Gwei(whistleblower_reward - proposer_reward)) -``` - ## Beacon chain state transition function From c7331a18b180cf6656f258cc0a1674481535728e Mon Sep 17 00:00:00 2001 From: Potuz Date: Sat, 26 Aug 2023 08:23:18 -0300 Subject: [PATCH 013/112] Check IL compatibility with parent block hash --- specs/_features/epbs/beacon-chain.md | 28 ++++++++++++++++++++++------ specs/_features/epbs/fork-choice.md | 18 +++++++++++------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 802bc926c5..cc93060a6a 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -47,7 +47,7 @@ For a further introduction please refer to this [ethresear.ch article](https://e | Name | Value | | - | - | -| `PTC_SIZE` | `uint64(2**9)` (=512) | +| `PTC_SIZE` | `uint64(2**9)` (=512) # (New in ePBS) | ### Domain types @@ -80,13 +80,13 @@ For a further introduction please refer to this [ethresear.ch article](https://e | Name | Value | | - | - | -| `PTC_PENALTY_WEIGHT` | `uint64(2)` | +| `PTC_PENALTY_WEIGHT` | `uint64(2)` # (New in ePBS)| ### Execution | Name | Value | | - | - | -| MAX_TRANSACTIONS_PER_INCLUSION_LIST | `2**4` (=16) | -| MAX_GAS_PER_INCLUSION_LIST | `2**21` (=2,097,152) | +| MAX_TRANSACTIONS_PER_INCLUSION_LIST | `2**4` (=16) # (New in ePBS) | +| MAX_GAS_PER_INCLUSION_LIST | `2**21` (=2,097,152) # (New in ePBS) | ## Containers @@ -130,6 +130,7 @@ class InclusionListSummaryEntry(Container): ```python class InclusionListSummary(Container) proposer_index: ValidatorIndex + parent_block_hash: Hash32 summary: List[InclusionListSummaryEntry, MAX_TRANSACTIONS_PER_INCLUSION_LIST] ``` @@ -281,6 +282,20 @@ class BeaconState(Container): current_signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] ``` +## Helper functions + +### Predicates + +#### `is_builder` + +```python +def is_builder(validator: Validator) -> bool: + """ + Check if `validator` is a registered builder + """ + return validator.withdrawal_credentials[0] == BUILDER_WITHDRAWAL_PREFIX +``` + ## 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. @@ -331,8 +346,9 @@ 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 current ``self.execution_state``, their 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 + 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 """ ... ``` diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 91774a6b2a..50b44bd297 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -27,6 +27,16 @@ def verify_inclusion_list(state: BeaconState, block: BeaconBlock, inclusion_list proposer_index = signed_summary.message.proposer_index assert block.proposer_index == proposer_index + # Check that the signature is correct + # TODO: do we need a new domain? + signing_root = compute_signing_root(signed_summary.message, get_domain(state, DOMAIN_BEACON_PROPOSER)) + proposer = state.validators[proposer_index] + assert bls.Verify(proposer.pubkey, signing_root, signed_summary.signature) + + # Check that the parent_hash corresponds to the state's last execution payload header + parent_hash = signed_summary.message.parent_block_hash + assert parent_hash == state.latest_execution_payload_header.block_hash + # 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 @@ -37,13 +47,7 @@ def verify_inclusion_list(state: BeaconState, block: BeaconBlock, inclusion_list # Check that the total gas limit is bounded total_gas_limit = sum( entry.gas_limit for entry in summary) assert total_gas_limit <= MAX_GAS_PER_INCLUSION_LIST - - # Check that the signature is correct - # TODO: do we need a new domain? - signing_root = compute_signing_root(signed_summary.message, get_domain(state, DOMAIN_BEACON_PROPOSER)) - proposer = state.validators[proposer_index] - assert bls.Verify(proposer.pubkey, signing_root, signed_summary.signature) - + # Check that the inclusion list is valid return execution_engine.notify_new_inclusion_list(inclusion_list) ``` From 8d6ce8b6b1dced81dc3cc60b843e1bfc676914b7 Mon Sep 17 00:00:00 2001 From: Potuz Date: Sat, 26 Aug 2023 08:27:49 -0300 Subject: [PATCH 014/112] Do not broadcast the parent_block_hash but use it as parameter to the EL --- specs/_features/epbs/beacon-chain.md | 2 +- specs/_features/epbs/fork-choice.md | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index cc93060a6a..3e65363b32 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -130,7 +130,6 @@ class InclusionListSummaryEntry(Container): ```python class InclusionListSummary(Container) proposer_index: ValidatorIndex - parent_block_hash: Hash32 summary: List[InclusionListSummaryEntry, MAX_TRANSACTIONS_PER_INCLUSION_LIST] ``` @@ -335,6 +334,7 @@ def process_epoch(state: BeaconState) -> None: @dataclass class NewInclusionListRequest(object): inclusion_list: InclusionList + parent_block_hash: Hash32 ``` #### Engine APIs diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 50b44bd297..7762f209b0 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -33,10 +33,6 @@ def verify_inclusion_list(state: BeaconState, block: BeaconBlock, inclusion_list proposer = state.validators[proposer_index] assert bls.Verify(proposer.pubkey, signing_root, signed_summary.signature) - # Check that the parent_hash corresponds to the state's last execution payload header - parent_hash = signed_summary.message.parent_block_hash - assert parent_hash == state.latest_execution_payload_header.block_hash - # 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 @@ -49,7 +45,8 @@ def verify_inclusion_list(state: BeaconState, block: BeaconBlock, inclusion_list assert total_gas_limit <= MAX_GAS_PER_INCLUSION_LIST # Check that the inclusion list is valid - return execution_engine.notify_new_inclusion_list(inclusion_list) + return execution_engine.notify_new_inclusion_list(NewInclusionListRequest( + inclusion_list=inclusion_list, parent_block_hash = state.latest_execution_payload_header.block_hash)) ``` ### `is_inclusion_list_available` From 759cdd62938b87af42d89b93664cb1d1458f30cc Mon Sep 17 00:00:00 2001 From: Potuz Date: Sat, 26 Aug 2023 09:15:55 -0300 Subject: [PATCH 015/112] process payment immediately --- specs/_features/epbs/beacon-chain.md | 8 +++++--- specs/_features/epbs/design.md | 7 ++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 3e65363b32..c72b19a03b 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -384,11 +384,13 @@ def verify_execution_payload_header_signature(state: BeaconState, signed_header: ```python def process_execution_payload_header(state: BeaconState, signed_header: SignedExecutionPayloadHeader) -> None: assert verify_execution_payload_header_signature(state, signed_header) - # Check that the builder has funds to cover the bid + # Check that the builder has funds to cover the bid and transfer the funds header = signed_header.message builder_index = header.builder_index - if state.balances[builder_index] < header.value: - return false + amount = header.value + assert state.balances[builder_index] >= amount: + decrease_balance(state, builder_index, amount) + increase_balance(state, proposer_index, amount) # Verify consistency of the parent hash with respect to the previous execution payload header assert header.parent_hash == state.latest_execution_payload_header.block_hash # Verify prev_randao diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index cd76fc8431..6d9cb5fb79 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -16,8 +16,13 @@ ePBS introduces forward inclusion lists for proposers to guarantee censor resist ## Builders -There is a new entity `Builder` that is a glorified validator required to have a higher stake and required to sign when producing execution payloads. +There is a new entity `Builder` that is a glorified validator (they are simply validators with a different withdrawal prefix `0x0b`) required to have a higher stake and required to sign when producing execution payloads. - Builders are also validators (otherwise their staked capital depreciates). - We onboard builders by simply turning validators into builders if they achieve the necessary minimum balance (this way we avoid two forks to onboard builders and keep the same deposit flow, avoid builders to skip the entry churn), we change their withdrawal prefix to be distinguished from normal validators. - We need to include several changes from the [MaxEB PR](https://github.com/michaelneuder/consensus-specs/pull/3) in order to account with builders having an increased balance that would otherwise depreciate. + +## Builder Payments + +Payments are processed unconditionally when processing the signed execution payload header. There are cases to study for possible same-slot unbundling even by an equivocation. Same slot unbundling can happen if the proposer equivocates, and propagates his equivocation after seeing the reveal of the builder which happens at 8 seconds. The next proposer has to build on full which can only happen by being dishonest. Honest validators will vote for the previous block not letting the attack succeed. The honest builder does not lose his bid as the block is reorged. + From 90b42e6d27c50f41f5ea5945b9f5ba31468628a4 Mon Sep 17 00:00:00 2001 From: Potuz Date: Sat, 26 Aug 2023 11:10:51 -0300 Subject: [PATCH 016/112] Deal with withdrawals --- specs/_features/epbs/beacon-chain.md | 101 +++++++++++++++++++++++++-- specs/_features/epbs/design.md | 13 ++++ 2 files changed, 110 insertions(+), 4 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index c72b19a03b..3753eed2be 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -171,6 +171,7 @@ class BeaconBlockBody(Container): signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] # [New in ePBS] ``` #### `ExecutionPayload` @@ -328,6 +329,15 @@ def process_epoch(state: BeaconState) -> None: #### Request data +##### New `NewWithdrawalsRequest` + +```python +@dataclass +class NewWithdrawalsRequest(object): + withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + parent_block_hash: Hash32 +``` + ##### New `NewInclusionListRequest` ```python @@ -339,6 +349,19 @@ class NewInclusionListRequest(object): #### Engine APIs +#### New `notify_withdrawals` + +TODO: Can we send this with FCU as parameters instead of a new engine method reorg resistant? We need to remove withdrawals from the payload attributes now. + +```python +def notify_withdrawals(self: ExecutionEngine, withdrawals: NewWithdrawalsRequest) -> None + """ + This call informs the EL that the next payload which is a grandchild of the current ``parent_block_hash`` + needs to include the listed withdrawals that have been already fulfilled in the CL + """ + ... +``` + #### New `notify_new_inclusion_list` ```python @@ -355,20 +378,92 @@ def notify_new_inclusion_list(self: ExecutionEngine, ### 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`. +*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, block.body) [Modified in ePBS] process_execution_payload_header(state, block.body.execution_payload_header) # [Modified in ePBS] - # Removed process_withdrawal in ePBS is processed during payload processing [Modified in ePBS] 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 `get_expected_withdrawals` +**Note:** the function `get_expected_withdrawals` is modified to return no withdrawals if the parent block was empty. +TODO: Still need to include the MaxEB changes + +```python +def get_expected_withdrawals(state: BeaconState) -> Sequence[Withdrawal]: + ## return early if the parent block was empty + withdrawals: List[Withdrawal] = [] + if state.current_signed_execution_payload_header.message != state.latest_execution_payload_header: + return withdrawals + epoch = get_current_epoch(state) + withdrawal_index = state.next_withdrawal_index + validator_index = state.next_withdrawal_validator_index + bound = min(len(state.validators), MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) + for _ in range(bound): + validator = state.validators[validator_index] + balance = state.balances[validator_index] + if is_fully_withdrawable_validator(validator, balance, epoch): + withdrawals.append(Withdrawal( + index=withdrawal_index, + validator_index=validator_index, + address=ExecutionAddress(validator.withdrawal_credentials[12:]), + amount=balance, + )) + withdrawal_index += WithdrawalIndex(1) + elif is_partially_withdrawable_validator(validator, balance): + withdrawals.append(Withdrawal( + index=withdrawal_index, + validator_index=validator_index, + address=ExecutionAddress(validator.withdrawal_credentials[12:]), + amount=balance - MAX_EFFECTIVE_BALANCE, + )) + withdrawal_index += WithdrawalIndex(1) + if len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD: + break + validator_index = ValidatorIndex((validator_index + 1) % len(state.validators)) + return withdrawals +``` + +#### Modified `process_withdrawals` +**Note:** TODO: This is modified to take a `BeaconBlockBody`. Still need to include the MaxEB changes + +```python +def process_withdrawals(state: BeaconState, body: BeaconBlockBody) -> None: + withdrawals = body.withdrawals + expected_withdrawals = get_expected_withdrawals(state) + assert len(withdrawals) == len(expected_withdrawals) + + for expected_withdrawal, withdrawal in zip(expected_withdrawals, withdrawals): + assert withdrawal == expected_withdrawal + decrease_balance(state, withdrawal.validator_index, withdrawal.amount) + + # Update the next withdrawal index if this block contained withdrawals + if len(expected_withdrawals) != 0: + latest_withdrawal = expected_withdrawals[-1] + state.next_withdrawal_index = WithdrawalIndex(latest_withdrawal.index + 1) + + # Update the next validator index to start the next withdrawal sweep + if len(expected_withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD: + # Next sweep starts after the latest withdrawal's validator index + next_validator_index = ValidatorIndex((expected_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 + # Inform the EL of the processed withdrawals + hash = body.signed_execution_payload_header.message.parent_block_hash + execution_engine.notify_withdrawals(NewWithdrawalsRequest(withdrawals=withdrawals, parent_block_hash = hash)) +``` + #### New `verify_execution_payload_header_signature` ```python @@ -431,8 +526,6 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti parent_beacon_block_root=state.latest_block_header.parent_root, ) ) - # Process Withdrawals in the payload - process_withdrawals(state, payload) # Cache the execution payload header state.latest_execution_payload_header = state.current_signed_execution_payload_header.message # Verify the state root diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index 6d9cb5fb79..71f420360e 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -26,3 +26,16 @@ There is a new entity `Builder` that is a glorified validator (they are simply v Payments are processed unconditionally when processing the signed execution payload header. There are cases to study for possible same-slot unbundling even by an equivocation. Same slot unbundling can happen if the proposer equivocates, and propagates his equivocation after seeing the reveal of the builder which happens at 8 seconds. The next proposer has to build on full which can only happen by being dishonest. Honest validators will vote for the previous block not letting the attack succeed. The honest builder does not lose his bid as the block is reorged. +## Withdrawals + +Withdrawals cannot be fulfilled at the same time in the CL and the EL now: if they are included in the Execution payload for slot N, the consensus block for the same slot may have invalidated the validator balances. The new mechanism is as follows +- Proposer for block N includes the withdrawals in his CL block and they are immediately processed in the CL +- The execution payload for N mints ETH in the EL for the already burnt ETH in the CL during slot N-1. +- If the slot N-1 was empty, the proposer for N does not advance withdrawals and does not include any new withdrawals, so that each EL block mints a maximum of `MAX_WITHDRAWALS_PER_PAYLOAD` withdrawals. +- There is a new engine endpoint that notifies the EL of the next withdrawals that need to be included. The EL needs to verify the validity that the execution payload includes those withdrawals in the next blockhash. + +### Examples + +(N-2, Full) -- (N-1: Full, withdrawals 0--15) -- (N: Empty, withdrawals 16--31) -- (N+1, does not send withdrawals, Full: excecutes withdrawals 0--15 since it is a child of the blockhash from N-1, thus grandchild of the blockhash of N-2) -- (N+2, withdrawals 32--47, Full: excecutes withdrawals 16--31) + +(N-2, Full) - (N-1: Full withdrawals 0--15) -- (N: Empty, withdrawals 16--31) -- (N+1: Empty, no withdrawals) -- (N+2: no withdrawals, Full: executes withdrawals 0--15). From a6044623bbe923c6acd79a5b4ff0165ba2f462ea Mon Sep 17 00:00:00 2001 From: Potuz Date: Sat, 26 Aug 2023 13:41:39 -0300 Subject: [PATCH 017/112] fix withdrawals, check only in the CL --- specs/_features/epbs/beacon-chain.md | 85 ++++++---------------------- specs/_features/epbs/design.md | 12 +--- 2 files changed, 19 insertions(+), 78 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 3753eed2be..537fd97b6a 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -171,7 +171,6 @@ class BeaconBlockBody(Container): signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] - withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] # [New in ePBS] ``` #### `ExecutionPayload` @@ -280,6 +279,7 @@ class BeaconState(Container): historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] # PBS current_signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] + last_withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] # [New in ePBS] ``` ## Helper functions @@ -349,19 +349,6 @@ class NewInclusionListRequest(object): #### Engine APIs -#### New `notify_withdrawals` - -TODO: Can we send this with FCU as parameters instead of a new engine method reorg resistant? We need to remove withdrawals from the payload attributes now. - -```python -def notify_withdrawals(self: ExecutionEngine, withdrawals: NewWithdrawalsRequest) -> None - """ - This call informs the EL that the next payload which is a grandchild of the current ``parent_block_hash`` - needs to include the listed withdrawals that have been already fulfilled in the CL - """ - ... -``` - #### New `notify_new_inclusion_list` ```python @@ -384,7 +371,7 @@ def notify_new_inclusion_list(self: ExecutionEngine, ```python def process_block(state: BeaconState, block: BeaconBlock) -> None: process_block_header(state, block) - process_withdrawals(state, block.body) [Modified in ePBS] + process_withdrawals(state) [Modified in ePBS] process_execution_payload_header(state, block.body.execution_payload_header) # [Modified in ePBS] process_randao(state, block.body) process_eth1_data(state, block.body) @@ -392,76 +379,34 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None: process_sync_aggregate(state, block.body.sync_aggregate) ``` -#### Modified `get_expected_withdrawals` -**Note:** the function `get_expected_withdrawals` is modified to return no withdrawals if the parent block was empty. -TODO: Still need to include the MaxEB changes +#### Modified `process_withdrawals` +**Note:** TODO: This is modified to take only the State as parameter as they are deterministic. Still need to include the MaxEB changes ```python -def get_expected_withdrawals(state: BeaconState) -> Sequence[Withdrawal]: +def process_withdrawals(state: BeaconState) -> None: ## return early if the parent block was empty - withdrawals: List[Withdrawal] = [] if state.current_signed_execution_payload_header.message != state.latest_execution_payload_header: - return withdrawals - epoch = get_current_epoch(state) - withdrawal_index = state.next_withdrawal_index - validator_index = state.next_withdrawal_validator_index - bound = min(len(state.validators), MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) - for _ in range(bound): - validator = state.validators[validator_index] - balance = state.balances[validator_index] - if is_fully_withdrawable_validator(validator, balance, epoch): - withdrawals.append(Withdrawal( - index=withdrawal_index, - validator_index=validator_index, - address=ExecutionAddress(validator.withdrawal_credentials[12:]), - amount=balance, - )) - withdrawal_index += WithdrawalIndex(1) - elif is_partially_withdrawable_validator(validator, balance): - withdrawals.append(Withdrawal( - index=withdrawal_index, - validator_index=validator_index, - address=ExecutionAddress(validator.withdrawal_credentials[12:]), - amount=balance - MAX_EFFECTIVE_BALANCE, - )) - withdrawal_index += WithdrawalIndex(1) - if len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD: - break - validator_index = ValidatorIndex((validator_index + 1) % len(state.validators)) - return withdrawals -``` - -#### Modified `process_withdrawals` -**Note:** TODO: This is modified to take a `BeaconBlockBody`. Still need to include the MaxEB changes - -```python -def process_withdrawals(state: BeaconState, body: BeaconBlockBody) -> None: - withdrawals = body.withdrawals - expected_withdrawals = get_expected_withdrawals(state) - assert len(withdrawals) == len(expected_withdrawals) - - for expected_withdrawal, withdrawal in zip(expected_withdrawals, withdrawals): - assert withdrawal == expected_withdrawal + return + withdrawals = get_expected_withdrawals(state) + state.last_withdrawals = 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(expected_withdrawals) != 0: - latest_withdrawal = expected_withdrawals[-1] + 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(expected_withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD: + if len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD: # Next sweep starts after the latest withdrawal's validator index - next_validator_index = ValidatorIndex((expected_withdrawals[-1].validator_index + 1) % len(state.validators)) + 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 - # Inform the EL of the processed withdrawals - hash = body.signed_execution_payload_header.message.parent_block_hash - execution_engine.notify_withdrawals(NewWithdrawalsRequest(withdrawals=withdrawals, parent_block_hash = hash)) ``` #### New `verify_execution_payload_header_signature` @@ -517,6 +462,10 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti hash = hash_tree_root(payload) previous_hash = hash_tree_root(state.current_signed_execution_payload_header.message) assert hash == previous_hash + # Verify the withdrawals + assert len(state.last_withrawals) == len(payload.withdrawals) + for withdrawal, payload_withdrawal in zip(state.last_withdrawals, payload.withdrawals): + assert withdrawal == payload_withdrawal # Verify the execution payload is valid versioned_hashes = [kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments] assert execution_engine.verify_and_notify_new_payload( diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index 71f420360e..25cb62179c 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -28,14 +28,6 @@ Payments are processed unconditionally when processing the signed execution payl ## Withdrawals -Withdrawals cannot be fulfilled at the same time in the CL and the EL now: if they are included in the Execution payload for slot N, the consensus block for the same slot may have invalidated the validator balances. The new mechanism is as follows -- Proposer for block N includes the withdrawals in his CL block and they are immediately processed in the CL -- The execution payload for N mints ETH in the EL for the already burnt ETH in the CL during slot N-1. -- If the slot N-1 was empty, the proposer for N does not advance withdrawals and does not include any new withdrawals, so that each EL block mints a maximum of `MAX_WITHDRAWALS_PER_PAYLOAD` withdrawals. -- There is a new engine endpoint that notifies the EL of the next withdrawals that need to be included. The EL needs to verify the validity that the execution payload includes those withdrawals in the next blockhash. +Withdrawals are deterministic on the beacon state, so on a consensus layer block processing, they are immediately processed, then later when the payload appears we verify that the withdrawals in the payload agree with the already fulfilled withdrawals in the CL. -### Examples - -(N-2, Full) -- (N-1: Full, withdrawals 0--15) -- (N: Empty, withdrawals 16--31) -- (N+1, does not send withdrawals, Full: excecutes withdrawals 0--15 since it is a child of the blockhash from N-1, thus grandchild of the blockhash of N-2) -- (N+2, withdrawals 32--47, Full: excecutes withdrawals 16--31) - -(N-2, Full) - (N-1: Full withdrawals 0--15) -- (N: Empty, withdrawals 16--31) -- (N+1: Empty, no withdrawals) -- (N+2: no withdrawals, Full: executes withdrawals 0--15). +So when importing the CL block for slot N, we process the expected withdrawals at that slot. We save the list of paid withdrawals to the beacon state. When the payload for slot N appears, we check that the withdrawals correspond to the saved withdrawals. If the payload does not appear, the saved withdrawals remain, so any future payload has to include those. From d6ca928c0418b9cd9a324656ee0a19aafc76724f Mon Sep 17 00:00:00 2001 From: Potuz Date: Sun, 27 Aug 2023 08:15:49 -0300 Subject: [PATCH 018/112] remove unused request dataclass --- specs/_features/epbs/beacon-chain.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 537fd97b6a..ac9d6436ee 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -329,15 +329,6 @@ def process_epoch(state: BeaconState) -> None: #### Request data -##### New `NewWithdrawalsRequest` - -```python -@dataclass -class NewWithdrawalsRequest(object): - withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] - parent_block_hash: Hash32 -``` - ##### New `NewInclusionListRequest` ```python From 70a6e6db416425c792f10d6459888d5f39181f87 Mon Sep 17 00:00:00 2001 From: Potuz Date: Sun, 27 Aug 2023 08:24:06 -0300 Subject: [PATCH 019/112] pass the propsoer_index to process_execution_payload_header --- specs/_features/epbs/beacon-chain.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index ac9d6436ee..49a9e0e49b 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -363,7 +363,7 @@ def notify_new_inclusion_list(self: ExecutionEngine, 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.body.execution_payload_header) # [Modified in ePBS] + process_execution_payload_header(state, block) # [Modified in ePBS] process_randao(state, block.body) process_eth1_data(state, block.body) process_operations(state, block.body) # [Modified in ePBS] @@ -413,7 +413,8 @@ def verify_execution_payload_header_signature(state: BeaconState, signed_header: #### New `process_execution_payload_header` ```python -def process_execution_payload_header(state: BeaconState, signed_header: SignedExecutionPayloadHeader) -> None: +def process_execution_payload_header(state: BeaconState, block: BeaconBlock) -> None: + 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 and transfer the funds header = signed_header.message @@ -421,7 +422,7 @@ def process_execution_payload_header(state: BeaconState, signed_header: SignedEx amount = header.value assert state.balances[builder_index] >= amount: decrease_balance(state, builder_index, amount) - increase_balance(state, proposer_index, amount) + increase_balance(state, block.proposer_index, amount) # Verify consistency of the parent hash with respect to the previous execution payload header assert header.parent_hash == state.latest_execution_payload_header.block_hash # Verify prev_randao From 2c4fc84093b95d160b6753f73caacab7a04809d5 Mon Sep 17 00:00:00 2001 From: Potuz Date: Sun, 27 Aug 2023 08:29:36 -0300 Subject: [PATCH 020/112] run doctoc --- specs/_features/epbs/beacon-chain.md | 45 ++++++++++++++++++++++++++++ specs/_features/epbs/fork-choice.md | 9 ++++++ 2 files changed, 54 insertions(+) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 49a9e0e49b..7b45aeea40 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -6,6 +6,51 @@ +- [Introduction](#introduction) +- [Constants](#constants) + - [Withdrawal prefixes](#withdrawal-prefixes) +- [Configuration](#configuration) + - [Time parameters](#time-parameters) +- [Preset](#preset) + - [Misc](#misc) + - [Domain types](#domain-types) + - [Gwei values](#gwei-values) + - [Time parameters](#time-parameters-1) + - [Rewards and penalties](#rewards-and-penalties) + - [Incentivization weights](#incentivization-weights) + - [Execution](#execution) +- [Containers](#containers) + - [New containers](#new-containers) + - [`SignedExecutionPayloadHeader`](#signedexecutionpayloadheader) + - [`ExecutionPayloadEnvelope`](#executionpayloadenvelope) + - [`SignedExecutionPayloadEnvelope`](#signedexecutionpayloadenvelope) + - [`InclusionListSummaryEntry`](#inclusionlistsummaryentry) + - [`InclusionListSummary`](#inclusionlistsummary) + - [`SignedInclusionListSummary`](#signedinclusionlistsummary) + - [`InclusionList`](#inclusionlist) + - [Modified containers](#modified-containers) + - [`BeaconBlockBody`](#beaconblockbody) + - [`ExecutionPayload`](#executionpayload) + - [`ExecutionPayloadHeader`](#executionpayloadheader) + - [`BeaconState`](#beaconstate) +- [Helper functions](#helper-functions) + - [Predicates](#predicates) + - [`is_builder`](#is_builder) +- [Beacon chain state transition function](#beacon-chain-state-transition-function) + - [Epoch processing](#epoch-processing) + - [Modified `process_epoch`](#modified-process_epoch) + - [Execution engine](#execution-engine) + - [Request data](#request-data) + - [New `NewInclusionListRequest`](#new-newinclusionlistrequest) + - [Engine APIs](#engine-apis) + - [New `notify_new_inclusion_list`](#new-notify_new_inclusion_list) + - [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) + - [New `verify_execution_payload_signature`](#new-verify_execution_payload_signature) + - [Modified `process_execution_payload`](#modified-process_execution_payload) + diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 7762f209b0..7532476e12 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -5,6 +5,15 @@ +- [Introduction](#introduction) +- [Helpers](#helpers) + - [`verify_inclusion_list`](#verify_inclusion_list) + - [`is_inclusion_list_available`](#is_inclusion_list_available) +- [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) + From 43a8dd6c91a1038df45c426903f4a63733d66551 Mon Sep 17 00:00:00 2001 From: Potuz Date: Sun, 27 Aug 2023 15:13:16 -0300 Subject: [PATCH 021/112] only cache the withdrawals root in the state --- specs/_features/epbs/beacon-chain.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 7b45aeea40..a20874d41e 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -222,6 +222,7 @@ class BeaconBlockBody(Container): **Note:** The `ExecutionPayload` is modified to contain the builder's index and the bid value. It also contains a transaction inclusion list summary signed by the corresponding beacon block proposer and the list of indices of transactions in the parent block that have to be excluded from the inclusion list summary because they were satisfied in the previous slot. +TODO: `builder_index` and `value` do not need to be in the payload sent to the engine, but they need to be in the header committed to the state. Either we move them out here to the envelope and we add them to back when comparing with the committed header, or we keep as here and we will be sending 16 extra bytes to the EL that are ignored. ```python class ExecutionPayload(Container): # Execution block header fields @@ -324,7 +325,7 @@ class BeaconState(Container): historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] # PBS current_signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] - last_withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] # [New in ePBS] + last_withdrawals_root: Root # [New in ePBS] ``` ## Helper functions @@ -424,7 +425,7 @@ def process_withdrawals(state: BeaconState) -> None: if state.current_signed_execution_payload_header.message != state.latest_execution_payload_header: return withdrawals = get_expected_withdrawals(state) - state.last_withdrawals = withdrawals + state.last_withdrawals_root = hash_tree_root(withdrawals) for withdrawal in withdrawals: decrease_balance(state, withdrawal.validator_index, withdrawal.amount) @@ -468,6 +469,8 @@ def process_execution_payload_header(state: BeaconState, block: BeaconBlock) -> assert state.balances[builder_index] >= amount: decrease_balance(state, builder_index, amount) increase_balance(state, block.proposer_index, amount) + # Verify the withdrawals_root against the state cached ones + assert header.withdrawals_root == state.last_withdrawals_root # Verify consistency of the parent hash with respect to the previous execution payload header assert header.parent_hash == state.latest_execution_payload_header.block_hash # Verify prev_randao @@ -499,10 +502,6 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti hash = hash_tree_root(payload) previous_hash = hash_tree_root(state.current_signed_execution_payload_header.message) assert hash == previous_hash - # Verify the withdrawals - assert len(state.last_withrawals) == len(payload.withdrawals) - for withdrawal, payload_withdrawal in zip(state.last_withdrawals, payload.withdrawals): - assert withdrawal == payload_withdrawal # Verify the execution payload is valid versioned_hashes = [kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments] assert execution_engine.verify_and_notify_new_payload( From 52067d58c36de0bbd4b6f9e83b15f483413ed5be Mon Sep 17 00:00:00 2001 From: Potuz Date: Sun, 27 Aug 2023 21:23:18 -0300 Subject: [PATCH 022/112] ptc rewards pass 1 --- specs/_features/epbs/beacon-chain.md | 207 ++++++++++++++++++++++++++- specs/_features/epbs/fork-choice.md | 19 ++- 2 files changed, 223 insertions(+), 3 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index a20874d41e..dd5d400ed8 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -98,7 +98,8 @@ For a further introduction please refer to this [ethresear.ch article](https://e | Name | Value | | - | - | -| `DOMAIN_BEACON_BUILDER` | `DomainType('0x0B000000')` # (New in ePBS)| +| `DOMAIN_BEACON_BUILDER` | `DomainType('0x1B000000')` # (New in ePBS)| +| `DOMAIN_PTC_ATTESTER` | `DomainType('0x0C000000')` # (New in ePBS)| ### Gwei values @@ -137,6 +138,41 @@ For a further introduction please refer to this [ethresear.ch article](https://e ### New containers +#### `PayloadAttestationData` + +```python +class PayloadAttestationData(Container): + beacon_block_root: Root + payload_present: bool +``` + +#### `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 @@ -342,6 +378,68 @@ def is_builder(validator: Validator) -> bool: return validator.withdrawal_credentials[0] == BUILDER_WITHDRAWAL_PREFIX ``` +#### `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 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) +``` + +### Beacon State accessors + +#### `get_ptc` + +```python +def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]: + """ + Get the ptc committee for the give ``slot`` + """ + beacon_committee = get_beacon_committee(state, slot, 0)[:PTC_SIZE] + validator_indices = [idx for idx in beacon_committee if not is_builder(idx)] + return validator_indices[:PTC_SIZE] +``` + +#### `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. @@ -415,6 +513,111 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None: process_operations(state, block.body) # [Modified in ePBS] process_sync_aggregate(state, block.body.sync_aggregate) ``` +#### 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) + 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 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 state.slot > 0 + assert data.beacon_block_root == state.block_roots[(state.slot - 1) % SLOTS_PER_HISTORICAL_ROOT] + + #Verify signature + indexed_payload_attestation = get_indexed_payload_attestation(state, state.slot - 1, payload_attestation) + assert is_valid_indexed_payload_attestation(state, indexed_payload_attestation) + + ptc = get_ptc(state, state.slot - 1) + if 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 + latest_payload_timestamp = state.latest_execution_payload_header.timestamp + present_timestamp = compute_timestamp_at_slot(state, state.slot - 1) + payload_was_present = latest_payload_timestamp == present_timestamp + if data.payload_present != payload_was_present: + return + # Reward the proposer and set all the participation flags + 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_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) +``` #### Modified `process_withdrawals` **Note:** TODO: This is modified to take only the State as parameter as they are deterministic. Still need to include the MaxEB changes @@ -422,7 +625,7 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None: ```python def process_withdrawals(state: BeaconState) -> None: ## return early if the parent block was empty - if state.current_signed_execution_payload_header.message != state.latest_execution_payload_header: + state.current_signed_execution_payload_header.message != state.latest_execution_payload_header: return withdrawals = get_expected_withdrawals(state) state.last_withdrawals_root = hash_tree_root(withdrawals) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 7532476e12..58d8705fae 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -50,7 +50,7 @@ def verify_inclusion_list(state: BeaconState, block: BeaconBlock, inclusion_list # TODO: These checks will also be performed by the EL surely so we can probably remove them from here. # Check that the total gas limit is bounded - total_gas_limit = sum( entry.gas_limit for entry in summary) + total_gas_limit = sum( entry.gas_limit for entry in summary ) assert total_gas_limit <= MAX_GAS_PER_INCLUSION_LIST # Check that the inclusion list is valid @@ -82,6 +82,13 @@ def is_inclusion_list_available(state: BeaconState, block: BeaconBlock) -> bool: return verify_inclusion_list(state, block, inclusion_list, EXECUTION_ENGINE) ``` +### `validate_on_payload_attestation` + +```python +def validate_ptc_from_block(store: Store, payload_attestation: PayloadAttestation) -> None: + # The beacon block root must be known + assert payload_attestation.data.beacon_block_root in store.blocks +``` ## Updated fork-choice handlers @@ -180,3 +187,13 @@ def on_excecution_payload(store: Store, signed_envelope_: SignedExecutionPayload #Add new state for this payload to the store store.execution_payload_states[beacon_block_root] = state ``` + +### `on_payload_attestation` + +```python +def on_payload_attestation(store: Store, ptc_attestation: PayloadAttestation) -> None + """ + Run ``on_payload_attestation`` upon receiving a new ``payload_attestation`` from either within a ``BeaconBlock`` + or directly on the wire. + """ + # From a3c24689e8258b0cf624d7ca54f45707300287e9 Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 28 Aug 2023 12:40:56 -0300 Subject: [PATCH 023/112] only pass relevant IL info to the EL --- specs/_features/epbs/beacon-chain.md | 3 ++- specs/_features/epbs/fork-choice.md | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index dd5d400ed8..18b85ddeab 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -478,7 +478,8 @@ def process_epoch(state: BeaconState) -> None: ```python @dataclass class NewInclusionListRequest(object): - inclusion_list: InclusionList + inclusion_list: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] + summary: List[InclusionListSummaryEntry, MAX_TRANSACTIONS_PER_INCLUSION_LIST] parent_block_hash: Hash32 ``` diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 58d8705fae..d064446b9a 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -55,7 +55,9 @@ def verify_inclusion_list(state: BeaconState, block: BeaconBlock, inclusion_list # Check that the inclusion list is valid return execution_engine.notify_new_inclusion_list(NewInclusionListRequest( - inclusion_list=inclusion_list, parent_block_hash = state.latest_execution_payload_header.block_hash)) + inclusion_list=inclusion_list.transactions, + summary=inclusion_list.summary.message.summary, + parent_block_hash = state.latest_execution_payload_header.block_hash)) ``` ### `is_inclusion_list_available` From 800a9101eb3df44339856e8f892ee36199adc55e Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 28 Aug 2023 16:47:32 -0300 Subject: [PATCH 024/112] Use envelopes instead of adding builder info to the payload Move the builder id and other unnecessary information outside of the payload. --- specs/_features/epbs/beacon-chain.md | 87 ++++++++++++++++++---------- specs/_features/epbs/design.md | 5 ++ specs/_features/epbs/fork-choice.md | 50 +++++++++++----- 3 files changed, 98 insertions(+), 44 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 18b85ddeab..9744798219 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -180,13 +180,31 @@ class SignedExecutionPayloadHeader(Container): message: ExecutionPayloadHeader signature: BLSSignature ``` +#### `ExecutionPayloadHeaderEnvelope` +```python +class ExecutionPayloadHeaderEnvelope(Container): + header: ExecutionPayloadHeader + builder_index: ValidatorIndex + value: Gwei +``` + +#### `SignedExecutionPayloadHeaderEnvelope` + +```python +class SignedExecutionPayloadHeaderEnvelope(Container): + message: ExecutionPayloadHeaderEnvelope + 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] state_root: Root ``` @@ -248,17 +266,16 @@ class BeaconBlockBody(Container): voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS] sync_aggregate: SyncAggregate # Execution - # Removed execution_payload [ Removed in ePBS] - signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] + # Removed execution_payload [Removed in ePBS] + # Removed blob_kzg_commitments [Removed in ePBS] + signed_execution_payload_header_envelope: SignedExecutionPayloadHeaderEnvelope # [New in ePBS] bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] - blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] ``` #### `ExecutionPayload` -**Note:** The `ExecutionPayload` is modified to contain the builder's index and the bid value. It also contains a transaction inclusion list summary signed by the corresponding beacon block proposer and the list of indices of transactions in the parent block that have to be excluded from the inclusion list summary because they were satisfied in the previous slot. +**Note:** The `ExecutionPayload` is modified to contain a transaction inclusion list summary signed by the corresponding beacon block proposer and the list of indices of transactions in the parent block that have to be excluded from the inclusion list summary because they were satisfied in the previous slot. -TODO: `builder_index` and `value` do not need to be in the payload sent to the engine, but they need to be in the header committed to the state. Either we move them out here to the envelope and we add them to back when comparing with the committed header, or we keep as here and we will be sending 16 extra bytes to the EL that are ignored. ```python class ExecutionPayload(Container): # Execution block header fields @@ -278,15 +295,15 @@ class ExecutionPayload(Container): block_hash: Hash32 # Hash of execution block transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD] withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] - builder_index: ValidatorIndex # [New in ePBS] - value: Gwei # [New in ePBS] + blob_gas_used: uint64 + excess_blob_gas: uint64 inclusion_list_summary: SignedInclusionListSummary # [New in ePBS] inclusion_list_exclusions: List[uint64, MAX_TRANSACTIONS_PER_INCLUSION_LIST] # [New in ePBS] ``` #### `ExecutionPayloadHeader` -**Note:** The `ExecutionPayloadHeader` is modified to include the builder's index and the bid's value. +**Note:** The `ExecutionPayloadHeader` is modified to account for the transactions inclusion lists. ```python class ExecutionPayloadHeader(Container): @@ -307,8 +324,8 @@ class ExecutionPayloadHeader(Container): block_hash: Hash32 # Hash of execution block transactions_root: Root withdrawals_root: Root - builder_index: ValidatorIndex # [New in ePBS] - value: Gwei # [New in ePBS] + blob_gas_used: uint64 + excess_blob_gas: uint64 inclusion_list_summary_root: Root # [New in ePBS] inclusion_list_exclusions_root: Root # [New in ePBS] ``` @@ -360,7 +377,7 @@ class BeaconState(Container): # Deep history valid from Capella onwards historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] # PBS - current_signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] + signed_execution_payload_header_envelope: SignedExecutionPayloadHeaderEnvelop # [New in ePBS] last_withdrawals_root: Root # [New in ePBS] ``` @@ -508,12 +525,13 @@ def notify_new_inclusion_list(self: ExecutionEngine, 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] + 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_operations` **Note:** `process_operations` is modified to process PTC attestations @@ -626,7 +644,7 @@ def process_payload_attestation(state: BeaconState, payload_attestation: Payload ```python def process_withdrawals(state: BeaconState) -> None: ## return early if the parent block was empty - state.current_signed_execution_payload_header.message != state.latest_execution_payload_header: + state.signed_execution_payload_header_envelope.message.header != state.latest_execution_payload_header: return withdrawals = get_expected_withdrawals(state) state.last_withdrawals_root = hash_tree_root(withdrawals) @@ -650,26 +668,27 @@ def process_withdrawals(state: BeaconState) -> None: state.next_withdrawal_validator_index = next_validator_index ``` -#### New `verify_execution_payload_header_signature` +#### New `verify_execution_payload_header_envelope_signature` ```python -def verify_execution_payload_header_signature(state: BeaconState, signed_header: SignedExecutionPayloadHeader) -> bool: +def verify_execution_payload_header_envelope_signature(state: BeaconState, + signed_header_envelope: SignedExecutionPayloadHeaderEnvelope) -> 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) + builder = state.validators[signed_header_envelope.message.builder_index] + signing_root = compute_signing_root(signed_header_envelope.message, get_domain(state, DOMAIN_BEACON_BUILDER)) + return bls.Verify(builder.pubkey, signing_root, signed_header_envelope.signature) ``` #### New `process_execution_payload_header` ```python def process_execution_payload_header(state: BeaconState, block: BeaconBlock) -> None: - signed_header = block.body.signed_execution_payload_header - assert verify_execution_payload_header_signature(state, signed_header) + signed_header_envelope = block.body.signed_execution_payload_header_envelope + assert verify_execution_payload_header_envelope_signature(state, signed_header_envelope) # Check that the builder has funds to cover the bid and transfer the funds - header = signed_header.message - builder_index = header.builder_index - amount = header.value + envelope = signed_header_envelope.message + builder_index = envelope.builder_index + amount = envelope.value assert state.balances[builder_index] >= amount: decrease_balance(state, builder_index, amount) increase_balance(state, block.proposer_index, amount) @@ -681,15 +700,15 @@ def process_execution_payload_header(state: BeaconState, block: BeaconBlock) -> assert header.prev_randao == get_randao_mix(state, get_current_epoch(state)) # Verify timestamp assert header.timestamp == compute_timestamp_at_slot(state, state.slot) - # Cache execution payload header - state.current_signed_execution_payload_header = signed_header + # Cache execution payload header envelope + state.signed_execution_payload_header_envelope = signed_header_envelope ``` #### New `verify_execution_payload_signature` ```python def verify_execution_envelope_signature(state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope) -> bool: - builder = state.validators[signed_envelope.message.payload.builder_index] + 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) ``` @@ -701,13 +720,19 @@ def verify_execution_envelope_signature(state: BeaconState, signed_envelope: Sig def process_execution_payload(state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope, execution_engine: ExecutionEngine) -> None: # Verify signature [New in ePBS] assert verify_execution_envelope_signature(state, signed_envelope) - payload = signed_envelope.message.payload + envelope = signed_envelope.message + payload = envelope.payload + # Verify consistency with the beacon block + assert envelope.beacon_block_root == hash_tree_root(state.latest_block_header) # Verify consistency with the committed header hash = hash_tree_root(payload) - previous_hash = hash_tree_root(state.current_signed_execution_payload_header.message) + commited_envelope = state.signed_execution_payload_header_envelope.message + previous_hash = hash_tree_root(committed_envelope.payload) assert hash == previous_hash + # Verify consistency with the envelope + assert envelope.builder_index == committed_envelope.builder_index # Verify the execution payload is valid - versioned_hashes = [kzg_commitment_to_versioned_hash(commitment) for commitment in body.blob_kzg_commitments] + 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, @@ -716,7 +741,7 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti ) ) # Cache the execution payload header - state.latest_execution_payload_header = state.current_signed_execution_payload_header.message + state.latest_execution_payload_header = committed_envelope.payload # Verify the state root - assert signed_envelope.message.state_root == hash_tree_root(state) + assert envelope.state_root == hash_tree_root(state) ``` diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index 25cb62179c..34c9b5f7f9 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -31,3 +31,8 @@ Payments are processed unconditionally when processing the signed execution payl Withdrawals are deterministic on the beacon state, so on a consensus layer block processing, they are immediately processed, then later when the payload appears we verify that the withdrawals in the payload agree with the already fulfilled withdrawals in the CL. So when importing the CL block for slot N, we process the expected withdrawals at that slot. We save the list of paid withdrawals to the beacon state. When the payload for slot N appears, we check that the withdrawals correspond to the saved withdrawals. If the payload does not appear, the saved withdrawals remain, so any future payload has to include those. + +## Blobs + +- KZG Commitments are now sent on the Execution Payload envelope broadcasted by the EL and the EL block can only be valid if the data is available. +- Blobs themselves may be broadcasted by the builder below as soon as it sees the beacon block if he sees it's safe. diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index d064446b9a..b665c927a6 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -23,6 +23,28 @@ This is the modification of the fork choice accompanying the ePBS upgrade. ## Helpers +### 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] +``` + ### `verify_inclusion_list` *[New in ePBS]* @@ -76,7 +98,7 @@ def is_inclusion_list_available(state: BeaconState, block: BeaconBlock) -> bool: `MIN_SLOTS_FOR_INCLUSION_LISTS_REQUESTS` """ # Verify that the list is empty if the parent consensus block did not contain a payload - if state.current_signed_execution_payload_header.message != state.latest_execution_payload_header: + if state.signed_execution_payload_header_envelope.message.header != state.latest_execution_payload_header: return true # verify the inclusion list @@ -98,6 +120,8 @@ def validate_ptc_from_block(store: Store, payload_attestation: PayloadAttestatio *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. There is also the addition of the inclusion list availability check. +In addition we delay the checking of blob availability until the processing of the execution payload. + ```python def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: """ @@ -109,10 +133,10 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: # Check if this blocks builds on empty or full parent block parent_block = store.blocks[block.parent_root] - parent_signed_payload_header = parent_block.body.signed_execution_payload_header - parent_payload_hash = paernt_signed_payload_header.message.block_hash - current_signed_payload_header = block.body.signed_execution_payload_header - current_payload_parent_hash = current_signed_payload_header.message.parent_hash + parent_signed_payload_header_envelope = parent_block.body.signed_execution_payload_header_envelope + parent_payload_hash = parent_signed_payload_header_envelope.message.header.block_hash + current_signed_payload_header_envelope = block.body.signed_execution_payload_header_envelope + current_payload_parent_hash = current_signed_payload_header_envelope.message.header.parent_hash # Make a copy of the state to avoid mutability issues if current_payload_parent_hash == parent_payload_hash: assert block.parent_root in store.execution_payload_states @@ -135,10 +159,6 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: ) assert store.finalized_checkpoint.root == finalized_checkpoint_block - # Check if blob data is available - # If not, this block MAY be queued and subsequently considered when blob data becomes available - assert is_data_available(hash_tree_root(block), block.body.blob_kzg_commitments) - # Check if there is a valid inclusion list. # This check is performed only if the block's slot is within the visibility window # If not, this block MAY be queued and subsequently considered when a valid inclusion list becomes available @@ -172,16 +192,20 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: ### `on_execution_payload` ```python -def on_excecution_payload(store: Store, signed_envelope_: SignedExecutionPayloadEnvelope) -> None: +def on_excecution_payload(store: Store, signed_envelope: SignedExecutionPayloadEnvelope) -> None: """ Run ``on_execution_payload`` upon receiving a new execution payload. """ - beacon_block_root = signed_envelope.beacon_block_root + envelope = signed_envelope.message # The corresponding beacon block root needs to be known - assert beacon_block_root in store.block_states + 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[beacon_block_root]) + state = copy(store.block_states[envelope.beacon_block_root]) # Process the execution payload process_execution_payload(state, signed_envelope, EXECUTION_ENGINE) From 0228ea0f6d791aef7961c67ff7356051856e53a4 Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 29 Aug 2023 10:23:40 -0300 Subject: [PATCH 025/112] ptc message handlers in forkchoice --- specs/_features/epbs/beacon-chain.md | 10 +++++- specs/_features/epbs/fork-choice.md | 49 +++++++++++++++++++++++----- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 9744798219..7956350ef6 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -122,6 +122,12 @@ For a further introduction please refer to this [ethresear.ch article](https://e | - | - | | `PROPOSER_EQUIVOCATION_PENALTY_FACTOR` | `uint64(2**2)` (= 4) # (New in ePBS)| +### Max operations per block + +| Name | Value | +| - | - | +| `MAX_PAYLOAD_ATTESTATIONS` | `2**1` (= 2) # (New in ePBS) | + ### Incentivization weights | Name | Value | @@ -268,8 +274,10 @@ class BeaconBlockBody(Container): # Execution # Removed execution_payload [Removed in ePBS] # Removed blob_kzg_commitments [Removed in ePBS] - signed_execution_payload_header_envelope: SignedExecutionPayloadHeaderEnvelope # [New in ePBS] bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] + # PBS + signed_execution_payload_header_envelope: SignedExecutionPayloadHeaderEnvelope # [New in ePBS] + payload_attestations: List[PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS] # [New in ePBS] ``` #### `ExecutionPayload` diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index b665c927a6..df466314da 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -106,12 +106,18 @@ def is_inclusion_list_available(state: BeaconState, block: BeaconBlock) -> bool: return verify_inclusion_list(state, block, inclusion_list, EXECUTION_ENGINE) ``` -### `validate_on_payload_attestation` +### `notify_ptc_messages` ```python -def validate_ptc_from_block(store: Store, payload_attestation: PayloadAttestation) -> None: - # The beacon block root must be known - assert payload_attestation.data.beacon_block_root in store.blocks +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 + """ + 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) ``` ## Updated fork-choice handlers @@ -174,6 +180,9 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: # Add new state for this block to the store store.block_states[block_root] = state + # 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 @@ -214,12 +223,34 @@ def on_excecution_payload(store: Store, signed_envelope: SignedExecutionPayloadE store.execution_payload_states[beacon_block_root] = state ``` -### `on_payload_attestation` +### `on_payload_attestation_message` ```python -def on_payload_attestation(store: Store, ptc_attestation: PayloadAttestation) -> None +def on_payload_attestation_message(store: Store, + ptc_message: PayloadAttestationMessage, is_from_block: bool=False) -> None: """ - Run ``on_payload_attestation`` upon receiving a new ``payload_attestation`` from either within a ``BeaconBlock`` - or directly on the wire. + 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, state.slot) + + # 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 state.slot == get_current_slot(store) + # Check that the attester is from the current ptc + assert ptc_message.validator_index in ptc + # 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 + # TODO: Do we want to slash ptc members that equivocate? + # we are updating here the message and so the last vote will be the one that counts. + ptc_index = ptc.index(ptc_message.validator_index) + ptc_vote = store.ptc_vote[data.beacon_block_root] + ptc_vote[ptc_index] = data.present +``` From 1ae9cd9f4aa1e4b3b22e72fcd28f3c283eb5f5e5 Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 29 Aug 2023 13:58:18 -0300 Subject: [PATCH 026/112] take ptc members from all committees --- specs/_features/epbs/beacon-chain.md | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 7956350ef6..42d07e0f1b 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -391,6 +391,20 @@ class BeaconState(Container): ## 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_builder` @@ -430,9 +444,16 @@ def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]: """ Get the ptc committee for the give ``slot`` """ - beacon_committee = get_beacon_committee(state, slot, 0)[:PTC_SIZE] - validator_indices = [idx for idx in beacon_committee if not is_builder(idx)] - return validator_indices[:PTC_SIZE] + epoch = compute_epoch_at_slot(slot) + committees_per_slot = bit_floor(max(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) + vals = [idx for idx in beacon_committee if not is_builder(idx)] + validator_indices += vals[:members_per_commitee] + return validator_indices ``` #### `get_payload_attesting_indices` From 2ed96787bfc7ebfee7b2b2e26019fcca31ef016e Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 29 Aug 2023 14:25:11 -0300 Subject: [PATCH 027/112] add forkchoice helper to get committee voted --- specs/_features/epbs/fork-choice.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index df466314da..66fe228f0e 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -21,6 +21,12 @@ This is the modification of the fork choice accompanying the ePBS upgrade. +## Constant + +| Name | Value | +| -------------------- | ----------- | +| `PAYLOAD_TIMELY_THRESHOLD` | `PTC_SIZE/2` (=`uint64(256)`) | + ## Helpers ### Modified `Store` @@ -43,6 +49,7 @@ class Store(object): 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] + ptc_vote: Dict[Root, Vector[bool, PTC_SIZE]] = field(default_factory=dict) # [New in ePBS] ``` ### `verify_inclusion_list` @@ -120,6 +127,19 @@ def notify_ptc_messages(store: Store, state: BeaconState, payload_attestations: 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 wether 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 ptc_vote[beacon_block_root].count(True) > PAYLOAD_TIMELY_THRESHOLD +``` + ## Updated fork-choice handlers ### `on_block` @@ -179,6 +199,8 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: 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] = [False]*PTC_SIZE # Notify the store about the payload_attestations in the block store.notify_ptc_messages(state, block.body.payload_attestations) From 4389af49b15bfe33f851730dccad5680fbee5aea Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 29 Aug 2023 14:59:12 -0300 Subject: [PATCH 028/112] add rewards and ptc attestations to the design doc --- specs/_features/epbs/design.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index 34c9b5f7f9..fb1c0beb7a 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -36,3 +36,14 @@ So when importing the CL block for slot N, we process the expected withdrawals a - KZG Commitments are now sent on the Execution Payload envelope broadcasted by the EL and the EL block can only be valid if the data is available. - Blobs themselves may be broadcasted by the builder below as soon as it sees the beacon block if he sees it's safe. + +## PTC Rewards +- PTC members are obtained as the first members from each beacon slot committee that are not builders. +- attesters are rewarded as a full attestation when they get the right payload presence: that is, if they vote for full (resp. empty) and the payload is included (resp. not included) then they get their participation bits (target, source and head timely) set. Otherwise they get a penalty as a missed attestation. +- Attestations to the CL block from these members are just ignored. + +## PTC Attestations + +There are two ways to import PTC attestations. CL blocks contain aggregates, called `PayloadAttestation` in the spec. And committee members broadcast unaggregated `PayloadAttestationMessage`s. The latter are only imported over the wire for the current slot, and the former are only imported on blocks for the previous slot. + + From 23c46d79b4e0dce4c1fca26d2c06d7ec64a09e64 Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 29 Aug 2023 15:00:05 -0300 Subject: [PATCH 029/112] doctoc --- specs/_features/epbs/beacon-chain.md | 19 ++++++++++++++++++- specs/_features/epbs/design.md | 15 +++++++++++++++ specs/_features/epbs/fork-choice.md | 5 +++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 42d07e0f1b..9931d6702a 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -17,11 +17,18 @@ - [Gwei values](#gwei-values) - [Time parameters](#time-parameters-1) - [Rewards and penalties](#rewards-and-penalties) + - [Max operations per block](#max-operations-per-block) - [Incentivization weights](#incentivization-weights) - [Execution](#execution) - [Containers](#containers) - [New containers](#new-containers) + - [`PayloadAttestationData`](#payloadattestationdata) + - [`PayloadAttestation`](#payloadattestation) + - [`PayloadAttestationMessage`](#payloadattestationmessage) + - [`IndexedPayloadAttestation`](#indexedpayloadattestation) - [`SignedExecutionPayloadHeader`](#signedexecutionpayloadheader) + - [`ExecutionPayloadHeaderEnvelope`](#executionpayloadheaderenvelope) + - [`SignedExecutionPayloadHeaderEnvelope`](#signedexecutionpayloadheaderenvelope) - [`ExecutionPayloadEnvelope`](#executionpayloadenvelope) - [`SignedExecutionPayloadEnvelope`](#signedexecutionpayloadenvelope) - [`InclusionListSummaryEntry`](#inclusionlistsummaryentry) @@ -34,8 +41,15 @@ - [`ExecutionPayloadHeader`](#executionpayloadheader) - [`BeaconState`](#beaconstate) - [Helper functions](#helper-functions) + - [Math](#math) + - [`bit_floor`](#bit_floor) - [Predicates](#predicates) - [`is_builder`](#is_builder) + - [`is_valid_indexed_payload_attestation`](#is_valid_indexed_payload_attestation) + - [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) - [Epoch processing](#epoch-processing) - [Modified `process_epoch`](#modified-process_epoch) @@ -45,8 +59,11 @@ - [Engine APIs](#engine-apis) - [New `notify_new_inclusion_list`](#new-notify_new_inclusion_list) - [Block processing](#block-processing) + - [Modified `process_operations`](#modified-process_operations) + - [Modified `process_attestation`](#modified-process_attestation) + - [Payload Attestations](#payload-attestations) - [Modified `process_withdrawals`](#modified-process_withdrawals) - - [New `verify_execution_payload_header_signature`](#new-verify_execution_payload_header_signature) + - [New `verify_execution_payload_header_envelope_signature`](#new-verify_execution_payload_header_envelope_signature) - [New `process_execution_payload_header`](#new-process_execution_payload_header) - [New `verify_execution_payload_signature`](#new-verify_execution_payload_signature) - [Modified `process_execution_payload`](#modified-process_execution_payload) diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index fb1c0beb7a..944cd3630d 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -1,3 +1,18 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [ePBS design notes](#epbs-design-notes) + - [Inclusion lists](#inclusion-lists) + - [Builders](#builders) + - [Builder Payments](#builder-payments) + - [Withdrawals](#withdrawals) + - [Blobs](#blobs) + - [PTC Rewards](#ptc-rewards) + - [PTC Attestations](#ptc-attestations) + + + # ePBS design notes ## Inclusion lists diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 66fe228f0e..310a1c3598 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -6,13 +6,18 @@ - [Introduction](#introduction) +- [Constant](#constant) - [Helpers](#helpers) + - [Modified `Store`](#modified-store) - [`verify_inclusion_list`](#verify_inclusion_list) - [`is_inclusion_list_available`](#is_inclusion_list_available) + - [`notify_ptc_messages`](#notify_ptc_messages) + - [`is_payload_present`](#is_payload_present) - [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) + - [`on_payload_attestation_message`](#on_payload_attestation_message) From 4fe85f84b887142ecb13d5d448b0ad8f33d4380a Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 29 Aug 2023 21:15:04 -0300 Subject: [PATCH 030/112] Design decisions in forkchoice --- specs/_features/epbs/beacon-chain.md | 2 +- specs/_features/epbs/design.md | 56 +++++++++++++++++++++++++++- specs/_features/epbs/fork-choice.md | 1 + 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 9931d6702a..37f74b2341 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -650,7 +650,7 @@ def process_payload_attestation(state: BeaconState, payload_attestation: Payload ## 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 + ## Check that the attestation is for the previous slot (TODO: Fix this to be the head block root or simply hurt the ptc members) assert state.slot > 0 assert data.beacon_block_root == state.block_roots[(state.slot - 1) % SLOTS_PER_HISTORICAL_ROOT] diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index 944cd3630d..2311abc801 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -61,4 +61,58 @@ So when importing the CL block for slot N, we process the expected withdrawals a There are two ways to import PTC attestations. CL blocks contain aggregates, called `PayloadAttestation` in the spec. And committee members broadcast unaggregated `PayloadAttestationMessage`s. The latter are only imported over the wire for the current slot, and the former are only imported on blocks for the previous slot. - +TODO: These import are current broken in the specs in that they only allow a PTC member of the current slot to vote for a beacon block of the current slot, that is, if their head was a previous beacon block, their vote will be disregarded completely and they will be penalized. + +## Forkchoice changes + +There are significant design decisions to make due to the fact that a slot can have 3 different statuses: + +1. The CL block is not included (therefore no payload can be included). This is a *skipped* slot. +2. The CL block is included and the payload is not revealed. This is an *empty* slot. +3. The CL block and the payload are both included. This is a *full* slot. + +Consider the following fork +```mermaid +graph RL +A[N-1, Full] +B[N, Empty] --> A +C[N, Full] --> A +D[N+1, Full] --> B +``` + +In this fork the proposer of `N+1` is attempting to reorg the payload of `N` that was seen by the majority of the PTC. Suppose that honest validators see that the PTC has voted `N` to be full. Then because of proposer boost, the CL block of `N+1` will have 40% of the committee to start with. Assuming perfect participation, honest validators should see a weight of `100` for `(N, Full)` and a weight of `40` for `N+1` (notice that they attest before seeing the payload). They should choose to vote for `(N, Full)` instead of `N+1`. The question is how do we account for all of this? A few initial comments are in line +- CL attestation do not mention full or empty they simply have a beacon block root. Honest validators will have already set their PTC vote during `N` that `N` was full. +- The only changes to the view of `N` as empty/full could come only when importing `N+1`, a beacon block that contains PTC Messages attesting for the payload of `N`. However, if honest validators have already achieved the threshold for `full`, they will consider the block full. +- This is one design decision: instead of having a hard threshold on the PTC (50% in the current spec) we could have a relative one, say for example a simple majority of the counted votes. This has some minor engineering problems (like keeping track of who voted in forkchoice more than simply if they voted for present or not), but this can easily be changed if there are some liveness concerns. +- The honest builder for `N+1` would not submit a bid here, since honest builders would have seen `N` as full also, they would only build on top of the blockhash included in `N`. +- The honest PTC members for `N+1` will vote for + +So the questions is what changes do we need to make to our current weight accounting so that we have `(N, Full)` and `(N+1, Full)` as viable for head above, but not `(N, Empty)`?. Moreover, we want `(N, Full)` to be the winner in the above situation. Before dwelling in the justification, let me say right away that a proposer for `N+2` would call `get_head` and would get `N.root`. And then he will call `is_payload_present(N.root)` and he will get `True` so he will propose based on `(N, Full)` reorging back the dishonest (malinformed) proposer of `N+1`. The implementation of `is_payload_present` is trivial so the only question is how to do LMD counting so that `N` beats `N+1` in the head computation. + +There are several notions that can be changed when we have *empty* or *full* slots. +- Checkpoints: + - we can consider checkpoints to be of the form `(Epoch, Root, bool)` where the `bool` is to indicate if the Beacon block was full or not. + - Another option is to consider checkpoints to be of the form `(Epoch, Root)` exactly as we do today, but only consider the last *full* block before or equal to the `Epoch` start. +Both have advantages and disadvantages, the second one allows for different contending states to be the first state of an epoch, but it keeps all implementations exactly as they are today. +- Ancestry computations, as in `get_ancestor`. + - We can change the signature of this function to be of the form `get_ancestor(Store, Root, slot) -> (Root, bool)` So that it returns the beacon block root and weather or not it is based on *Full* or *Empty*. + - Otherwise we can simply return the last *Full* block in the line of ancestry. Again there are advantages and disadvantages. In this last case, it would be very hard to know if two given blocks with a given payload status are in the same chain or not. + + +The proposal I am considering at this moment is the following: +- Keep checkpoints as `(Epoch, Root) ` and allow different start of epoch blocks. +- An honest validator, when requesting the state at a given block root, will use its canonical state. That is computed as follows. In the example above, when requesting the state with block root `N`, if a call to `get_head` returned `N+1` then the validator would return the `store.block_states[N.root]` which corresponds to `N, Empty`. If instead returned `N` then it would return the state `store.execution_payload_states[N.root]` which corresponds to `N, Full`. +- Thus, when requesting the *justified state* for example, it will use the state that actually corresponds to its canonical chain and he needs to track only `Epoch, Root` for checkpoints, with minimal code changes. +- For LMD accounting, the proposal is to keep weights exactly as today with one exception: direct votes for `N` are *only* counted in the chains supporting `N, Full` or `N, Empty` according to the PTC vote. So, in the fork above, any honest validator that voted for `N` during slot `N` will be counted in the chain for `N, Full`, but not in the chain of `N+1, Full`. Honest validators during `N+1` will also vote for `N`, and also counting their votes in for `N, Full` and not the attacker's `N+1`. Suppose the chain advances with two more bad blocks as follows +```mermaid +graph RL +A[N-1, Full] +B[N, Empty] --> A +C[N, Full] --> A +D[N+1, Full] --> B +G[N+1, Empty] --> B +E[N+2, Full] --> D +F[N+3, Full] --> G +F ~~~ E +``` +In this case all the attesters for `N+1` will be counted depending on the PTC members that voted for `(N+1, Full)`. Assuming honest PTC members, they would have voted for `N` during `N+1` so any CL attesters for `N+1` would be voting for `N+1, Empty` thus only counting for the head in `(N+3, Full)`. diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 310a1c3598..1c829ae827 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -252,6 +252,7 @@ def on_excecution_payload(store: Store, signed_envelope: SignedExecutionPayloadE ### `on_payload_attestation_message` +TODO: Fix this to allow votes for the head block? (or simply hurt the ptc members) ```python def on_payload_attestation_message(store: Store, ptc_message: PayloadAttestationMessage, is_from_block: bool=False) -> None: From 4eaddd9b6c8f6578d26cce8640016c47257506a2 Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 29 Aug 2023 21:22:37 -0300 Subject: [PATCH 031/112] checkpoint descriptions --- specs/_features/epbs/design.md | 1 + 1 file changed, 1 insertion(+) diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index 2311abc801..4479d4f34b 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -94,6 +94,7 @@ There are several notions that can be changed when we have *empty* or *full* slo - we can consider checkpoints to be of the form `(Epoch, Root, bool)` where the `bool` is to indicate if the Beacon block was full or not. - Another option is to consider checkpoints to be of the form `(Epoch, Root)` exactly as we do today, but only consider the last *full* block before or equal to the `Epoch` start. Both have advantages and disadvantages, the second one allows for different contending states to be the first state of an epoch, but it keeps all implementations exactly as they are today. + - A third approach, which seems the best so far, is to keep `(Epoch, Root)` and let head of the chain determine if it is *Full* or *Empty* as described below. - Ancestry computations, as in `get_ancestor`. - We can change the signature of this function to be of the form `get_ancestor(Store, Root, slot) -> (Root, bool)` So that it returns the beacon block root and weather or not it is based on *Full* or *Empty*. - Otherwise we can simply return the last *Full* block in the line of ancestry. Again there are advantages and disadvantages. In this last case, it would be very hard to know if two given blocks with a given payload status are in the same chain or not. From d9e02784cd901e19c19ffd3d013f9ad591537cec Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 30 Aug 2023 08:28:08 -0300 Subject: [PATCH 032/112] fix PTC rewards for old blocks --- specs/_features/epbs/beacon-chain.md | 14 +++++++------- specs/_features/epbs/design.md | 6 +++--- specs/_features/epbs/fork-choice.md | 10 ++++++---- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 37f74b2341..e17d310031 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -166,6 +166,7 @@ For a further introduction please refer to this [ethresear.ch article](https://e ```python class PayloadAttestationData(Container): beacon_block_root: Root + slot: Slot payload_present: bool ``` @@ -650,23 +651,22 @@ def process_payload_attestation(state: BeaconState, payload_attestation: Payload ## 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 (TODO: Fix this to be the head block root or simply hurt the ptc members) - assert state.slot > 0 - assert data.beacon_block_root == state.block_roots[(state.slot - 1) % SLOTS_PER_HISTORICAL_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, state.slot - 1, payload_attestation) + 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, state.slot - 1) - if slot % SLOTS_PER_EPOCH == 0: + 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 latest_payload_timestamp = state.latest_execution_payload_header.timestamp - present_timestamp = compute_timestamp_at_slot(state, state.slot - 1) + present_timestamp = compute_timestamp_at_slot(state, data.slot) payload_was_present = latest_payload_timestamp == present_timestamp if data.payload_present != payload_was_present: return diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index 4479d4f34b..626b17f4ad 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -10,6 +10,7 @@ - [Blobs](#blobs) - [PTC Rewards](#ptc-rewards) - [PTC Attestations](#ptc-attestations) + - [Forkchoice changes](#forkchoice-changes) @@ -61,8 +62,6 @@ So when importing the CL block for slot N, we process the expected withdrawals a There are two ways to import PTC attestations. CL blocks contain aggregates, called `PayloadAttestation` in the spec. And committee members broadcast unaggregated `PayloadAttestationMessage`s. The latter are only imported over the wire for the current slot, and the former are only imported on blocks for the previous slot. -TODO: These import are current broken in the specs in that they only allow a PTC member of the current slot to vote for a beacon block of the current slot, that is, if their head was a previous beacon block, their vote will be disregarded completely and they will be penalized. - ## Forkchoice changes There are significant design decisions to make due to the fact that a slot can have 3 different statuses: @@ -85,7 +84,8 @@ In this fork the proposer of `N+1` is attempting to reorg the payload of `N` tha - The only changes to the view of `N` as empty/full could come only when importing `N+1`, a beacon block that contains PTC Messages attesting for the payload of `N`. However, if honest validators have already achieved the threshold for `full`, they will consider the block full. - This is one design decision: instead of having a hard threshold on the PTC (50% in the current spec) we could have a relative one, say for example a simple majority of the counted votes. This has some minor engineering problems (like keeping track of who voted in forkchoice more than simply if they voted for present or not), but this can easily be changed if there are some liveness concerns. - The honest builder for `N+1` would not submit a bid here, since honest builders would have seen `N` as full also, they would only build on top of the blockhash included in `N`. -- The honest PTC members for `N+1` will vote for +- The honest PTC members for `N+1` will vote for `N, Full` they will be rewarded but they will not change the forkchoice view that `N` was already full. +- PTC members voting for a previous blockroot cannot change the forkchoice view of the payload status either way. So the questions is what changes do we need to make to our current weight accounting so that we have `(N, Full)` and `(N+1, Full)` as viable for head above, but not `(N, Empty)`?. Moreover, we want `(N, Full)` to be the winner in the above situation. Before dwelling in the justification, let me say right away that a proposer for `N+2` would call `get_head` and would get `N.root`. And then he will call `is_payload_present(N.root)` and he will get `True` so he will propose based on `(N, Full)` reorging back the dishonest (malinformed) proposer of `N+1`. The implementation of `is_payload_present` is trivial so the only question is how to do LMD counting so that `N` beats `N+1` in the head computation. diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 1c829ae827..502d4973f0 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -252,7 +252,6 @@ def on_excecution_payload(store: Store, signed_envelope: SignedExecutionPayloadE ### `on_payload_attestation_message` -TODO: Fix this to allow votes for the head block? (or simply hurt the ptc members) ```python def on_payload_attestation_message(store: Store, ptc_message: PayloadAttestationMessage, is_from_block: bool=False) -> None: @@ -263,18 +262,21 @@ def on_payload_attestation_message(store: Store, 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, state.slot) + 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 # 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 state.slot == get_current_slot(store) + assert data.slot == get_current_slot(store) # Check that the attester is from the current ptc assert ptc_message.validator_index in ptc # Verify the signature assert is_valid_indexed_payload_attestation(state, IndexedPayloadAttestation(attesting_indices = [ptc_message.validator_index], data = data, - signature = ptc_message signature)) + signature = ptc_message.signature)) # Update the ptc vote for the block # TODO: Do we want to slash ptc members that equivocate? # we are updating here the message and so the last vote will be the one that counts. From 4060a9b2f8126e91e3f613cf40183e0a2f786dc5 Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 30 Aug 2023 10:04:06 -0300 Subject: [PATCH 033/112] update forkchoice weights, take 1 --- specs/_features/epbs/fork-choice.md | 106 +++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 502d4973f0..9274304a23 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -144,7 +144,111 @@ def is_payload_present(store: Store, beacon_block_root: Root) -> bool: assert beacon_block_root in store.ptc_vote return ptc_vote[beacon_block_root].count(True) > PAYLOAD_TIMELY_THRESHOLD ``` - + +### 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) -> tuple[Root, bool]: + """ + returns the beacon block root of the ancestor of the beacon block with ``root`` at``slot`` and it also + returns ``true`` if it based on a full block and ``false`` otherwise. + If the beacon block with ``root`` is already at ``slot`` it returns it's PTC status. + """ + block = store.blocks[root] + if block.slot == slot: + return [root, store.is_payload_present(root)] + parent = store.blocks[block.parent_root] + if parent.slot > slot: + return get_ancestor(store, block.parent_root, slot) + if block.body.signed_execution_payload_header_envelope.message.parent_hash == + parent.body.signed_execution_payload_header_envelope.message.block_hash: + return (block.parent_root, True) + return (block.parent_root, False) +``` + +### 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) + (ancestor_root,_) = get_ancestor(store, root, epoch_first_slot) + return ancestor_root +``` + + +### `is_supporting_vote` + +```python +def is_supporting_vote(store: Store, root: Root, is_payload_present: bool, message_root: Root) -> bool: + """ + returns whether a vote for ``message_root`` supports the chain containing the beacon block ``root`` with the + payload contents indicated by ``is_payload_present``. + """ + (ancestor_root, is_ancestor_full) = get_ancestor(store, message_root, store.blocks[root].slot) + return (root == ancestor_root) and (is_payload_preset == is_ancestor_full) +``` + +### Modified `get_weight` + +**Note:** `get_weight` is modified to only count votes for descending chains that support the status of a pair `Root, bool`, where the `bool` indicates if the block was full or not. + +```python +def get_weight(store: Store, root: Root, is_payload_present: bool) -> 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 state.validators[i].slashed + ] + 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, root, is_payload_present, store.latest_messages[i].root)) + )) + if store.proposer_boost_root == Root(): + # Return only attestation score if ``proposer_boost_root`` is not set + return attestation_score + + # Calculate proposer score if ``proposer_boost_root`` is set + proposer_score = Gwei(0) + # Boost is applied if ``root`` is an ancestor of ``proposer_boost_root`` + (ancestor_root, is_ancestor_full) = get_ancestor(store, store.proposer_boost_root, store.blocks[root].slot) + if (ancestor_root == root) and (is_ancestor_full == is_payload_present): + committee_weight = get_total_active_balance(state) // SLOTS_PER_EPOCH + proposer_score = (committee_weight * PROPOSER_SCORE_BOOST) // 100 + return attestation_score + proposer_score +``` + +### Modified `get_head` + +**Note:** `get_head` is modified 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. + +```python +def get_head(store: Store) -> tuple[Root, bool]: + # Get filtered block tree that only includes viable branches + blocks = get_filtered_block_tree(store) + # Execute the LMD-GHOST fork choice + head_root = store.justified_checkpoint.root + head_full = is_payload_present(store, head_root) + while True: + children = [ + (root, present) for root in blocks.keys() + if blocks[root].parent_root == head for present in (True, False) + ] + if len(children) == 0: + return (head_root, head_full) + # Sort by latest attesting balance with ties broken lexicographically + # Ties broken by favoring block with lexicographically higher root + # Ties then broken by favoring full blocks + # TODO: Can (root, full), (root, empty) have the same weight? + head = max(children, key=lambda (root, present): (get_weight(store, root, present), root, present)) +``` + ## Updated fork-choice handlers ### `on_block` From f3006caf2911255c0762a8527b0958f3ca20702f Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 30 Aug 2023 10:05:39 -0300 Subject: [PATCH 034/112] doctoc updated --- specs/_features/epbs/fork-choice.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 9274304a23..3bf5231f09 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -13,6 +13,11 @@ - [`is_inclusion_list_available`](#is_inclusion_list_available) - [`notify_ptc_messages`](#notify_ptc_messages) - [`is_payload_present`](#is_payload_present) + - [Modified `get_ancestor`](#modified-get_ancestor) + - [Modified `get_checkpoint_block`](#modified-get_checkpoint_block) + - [`is_supporting_vote`](#is_supporting_vote) + - [Modified `get_weight`](#modified-get_weight) + - [Modified `get_head`](#modified-get_head) - [Updated fork-choice handlers](#updated-fork-choice-handlers) - [`on_block`](#on_block) - [New fork-choice handlers](#new-fork-choice-handlers) From 5004908abc6f0e265902cfc86cb583d9b8633528 Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 30 Aug 2023 11:52:36 -0300 Subject: [PATCH 035/112] add head_root --- specs/_features/epbs/fork-choice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 3bf5231f09..e0b25c321d 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -243,7 +243,7 @@ def get_head(store: Store) -> tuple[Root, bool]: while True: children = [ (root, present) for root in blocks.keys() - if blocks[root].parent_root == head for present in (True, False) + if blocks[root].parent_root == head_root for present in (True, False) ] if len(children) == 0: return (head_root, head_full) From dc7b24c7b76a6729f0e8819d3f440e243ef67141 Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 1 Sep 2023 13:45:41 -0300 Subject: [PATCH 036/112] add comment on equivocation --- specs/_features/epbs/design.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index 626b17f4ad..7ee22d7a4e 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -117,3 +117,13 @@ F[N+3, Full] --> G F ~~~ E ``` In this case all the attesters for `N+1` will be counted depending on the PTC members that voted for `(N+1, Full)`. Assuming honest PTC members, they would have voted for `N` during `N+1` so any CL attesters for `N+1` would be voting for `N+1, Empty` thus only counting for the head in `(N+3, Full)`. + +## Equivocations + +There is no need to do anything about proposer equivocations. Builders should reveal their block anyway. + +- At the time of reveal, the builder already has counted attestations for the current CL blocks, even if there are or not equivocations. Any equivocation available at this time will not have transactions that can unbundle him since he hasn't revealed. +- If the original block to which the builder committed is included, then the builder doesn't lose anything, that was the original intent. So if the original block is the overwhelming winner at the time of reveal, the builder can simply reveal and be safe that if there are any equivocations anyway his block was included. +- If the builder reveals, he knows that he can never be unbundled unless the next committee has a majority of malicious validators: attestations will go for an empty block before a block that is revealed after 8 seconds. +- So since the builder cannot be unbundled, then he either doesn't pay if the block is not included, or pays and its included. +- The splitting grief, that is, the proposer's block has about 50% of the vote at 8 seconds, remains. From 169e02b44cac47fb1f990066e14d5e40bb74cf07 Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 1 Sep 2023 13:47:33 -0300 Subject: [PATCH 037/112] remove signed execution payload header --- specs/_features/epbs/beacon-chain.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index e17d310031..4cbcca8eeb 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -197,13 +197,6 @@ class IndexedPayloadAttestation(Container): signature: BLSSignature ``` -#### `SignedExecutionPayloadHeader` - -```python -class SignedExecutionPayloadHeader(Container): - message: ExecutionPayloadHeader - signature: BLSSignature -``` #### `ExecutionPayloadHeaderEnvelope` ```python From a7e3ba13a689c718d65824bccc13b369c4ec2b28 Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 1 Sep 2023 13:49:19 -0300 Subject: [PATCH 038/112] typos --- specs/_features/epbs/beacon-chain.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 4cbcca8eeb..9363184a45 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -77,7 +77,7 @@ This is the beacon chain specification of the enshrined proposer builder separat *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 as opposed to the current three. Honest validators gather *signed bids* from builders and submit their consensus blocks (a `SignedBlindedBeaconBlock`) 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. +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 as opposed to the current three. Honest validators gather *signed bids* from builders and submit their consensus blocks (a `SigneddBeaconBlock`) 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 *full* block from a previous slot (eg. the current slot's proposer did not submit its block). @@ -453,7 +453,7 @@ def is_valid_indexed_payload_attestation(state: BeaconState, indexed_payload_att ```python def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]: """ - Get the ptc committee for the give ``slot`` + Get the ptc committee for the given ``slot`` """ epoch = compute_epoch_at_slot(slot) committees_per_slot = bit_floor(max(get_committee_count_per_slot(state, epoch), PTC_SIZE)) From 837897eaa6870bfc8df292c1ab7f39c8ca85dd88 Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 4 Sep 2023 18:00:33 -0300 Subject: [PATCH 039/112] add max EB changes --- specs/_features/epbs/beacon-chain.md | 609 +++++++++++++++++++++- specs/_features/epbs/design.md | 3 + specs/_features/epbs/fork-choice.md | 2 +- specs/_features/epbs/weak-subjectivity.md | 52 ++ 4 files changed, 655 insertions(+), 11 deletions(-) create mode 100644 specs/_features/epbs/weak-subjectivity.md diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 9363184a45..c57150cb19 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -94,6 +94,11 @@ For a further introduction please refer to this [ethresear.ch article](https://e | - | - | | `BUILDER_WITHDRAWAL_PREFIX` | `Bytes1('0x0b')` # (New in ePBS) | +### Slashing flags +| Name | Value | +| - | - | +| `SLASHED_ATTESTER_FLAG_INDEX`| `0` # (New in ePBS)| +| `SLASHED_PROPOSER_FLAG_INDEX`| `1` # (New in ePBS)| ## Configuration @@ -133,6 +138,12 @@ For a further introduction please refer to this [ethresear.ch article](https://e | - | - | :-: | :-: | | `MIN_SLOTS_FOR_INCLUSION_LISTS_REQUESTS` | `uint64(2)` | slots | 32 seconds # (New in ePBS) | +### State list lenghts +| Name | Value | Unit | Duration | +| - | - | :-: | :-: | +| `MAX_PENDING_BALANCE_DEPOSITS` | `uint64(2**20) = 1 048 576` | `PendingBalanceDeposits` | #(New in ePBS) | +| `MAX_PENDING_PARTIAL_WITHDRAWALS` | `uint64(2**20) = 1 048 576` | `PartialWithdrawals` | # (New in ePBS) | + ### Rewards and penalties | Name | Value | @@ -161,6 +172,32 @@ For a further introduction please refer to this [ethresear.ch article](https://e ### New containers +#### `PendingBalanceDeposit` + +```python +class PendingBalanceDeposit(Container): + index: ValidatorIndex + amount: Gwei +``` + +#### `PartialWithdrawal` + +```python +class PartialWithdrawal(Container) + index: ValidatorIndex + amount: Gwei + withdrawable_epoch: Epoch +``` + +#### `ExecutionLayerWithdrawalRequest` + +```python +class ExecutionLayerWithdrawalRequest(Container) + source_address: ExecutionAddress + validator_pubkey: BLSPubkey + balance: Gwei +``` + #### `PayloadAttestationData` ```python @@ -267,6 +304,23 @@ class InclusionList(Container) ### Modified containers +#### `Validator` +**Note:** The `Validator` class is modified to keep track of the slashed categories. + +```python +class Validator(Container): + pubkey: BLSPubkey + withdrawal_credentials: Bytes32 # Commitment to pubkey for withdrawals + effective_balance: Gwei # Balance at stake + slashed: uint8 # (Modified in ePBS) + # Status epochs + activation_eligibility_epoch: Epoch # When criteria for activation were met + activation_epoch: Epoch + exit_epoch: Epoch + withdrawable_epoch: Epoch # When validator can withdraw funds +``` + + #### `BeaconBlockBody` **Note:** The Beacon Block body is modified to contain a Signed `ExecutionPayloadHeader`. The containers `BeaconBlock` and `SignedBeaconBlock` are modified indirectly. @@ -350,7 +404,7 @@ class ExecutionPayloadHeader(Container): ``` #### `BeaconState` -*Note*: the beacon state is modified to store a signed latest execution payload header. +*Note*: the beacon state is modified to store a signed latest execution payload header, to track the last withdrawals and increased Maximum effective balance fields: `deposit_balance_to_consume`, `exit_balance_to_consume` and `earliest_exit_epoch`. ```python class BeaconState(Container): @@ -398,6 +452,11 @@ class BeaconState(Container): # PBS signed_execution_payload_header_envelope: SignedExecutionPayloadHeaderEnvelop # [New in ePBS] last_withdrawals_root: Root # [New in ePBS] + deposit_balance_to_consume: Gwei # [New in ePBS] + exit_balance_to_consume: Gwei # [New in ePBS] + earliest_exit_epoch: Epoch # [New in ePBS] + pending_balance_deposits: List[PendingBalanceDeposit, MAX_PENDING_BALANCE_DEPOSITS] # [New in ePBS] + pending_partial_withdrawals: List[PartialWithdrawals, MAX_PENDING_PARTIAL_WITHDRAWALS] # [New in ePBS] ``` ## Helper functions @@ -428,6 +487,77 @@ def is_builder(validator: Validator) -> bool: return validator.withdrawal_credentials[0] == BUILDER_WITHDRAWAL_PREFIX ``` +#### `is_eligible_for_activation_queue` + +```python +def is_eligible_for_activation_queue(validator: Validator) -> bool: + """ + Check if ``validator`` is eligible to be placed into the activation queue. + """ + return ( + validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH + and validator.effective_balance >= MIN_ACTIVATION_BALANCE + ) +``` + +#### `is_slashed_proposer` + +```python +def is_slashed_proposer(validator: Validator) -> bool: + """ + return ``true`` if ``validator`` has committed a proposer equivocation + """ + return has_flag(ParticipationFlags(validator.slashed), SLASHED_PROPOSER_FLAG_INDEX) +``` + +#### `is_slashed_attester` + +```python +def is_slashed_attester(validator: Validator) -> bool: + """ + return ``true`` if ``validator`` has committed an attestation slashing offense + """ + return has_flag(ParticipationFlags(validator.slashed), SLASHED_ATTESTSER_FLAG_INDEX) +``` + + +#### Modified `is_slashable_validator` +**Note:** The function `is_slashable_validator` is modified and renamed to `is_attester_slashable_validator`. + +```python +def is_attester_slashable_validator(validator: Validator, epoch: Epoch) -> bool: + """ + Check if ``validator`` is slashable. + """ + return (not is_slashed_attester(validator)) and (validator.activation_epoch <= epoch < validator.withdrawable_epoch) +``` + +#### Modified `is_fully_withdrawable_validator` + +```python +def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool: + """ + Check if ``validator`` is fully withdrawable. + """ + return ( + (has_eth1_withdrawal_credential(validator) or is_builder(validator)) + and validator.withdrawable_epoch <= epoch + and balance > 0 + ) +``` + +#### `is_partially_withdrawable_validator` + +```python +def is_partially_withdrawable_validator(validator: Validator, balance: Gwei) -> bool: + """ + Check if ``validator`` is partially withdrawable. + """ + if not (has_eth1_withdrawal_credential(validator) or is_builder(validator)): + return False + return get_validator_excess_balance(validator, balance) > 0 +``` + #### `is_valid_indexed_payload_attestation` ```python @@ -448,6 +578,18 @@ def is_valid_indexed_payload_attestation(state: BeaconState, indexed_payload_att ### Beacon State accessors +#### Modified `get_eligible_validator_indices` +**Note:** The function `get_eligible_validator_indices` is modified to use the new flag mechanism for slashings. + +```python +def get_eligible_validator_indices(state: BeaconState) -> Sequence[ValidatorIndex]: + previous_epoch = get_previous_epoch(state) + return [ + ValidatorIndex(index) for index, v in enumerate(state.validators) + if is_active_validator(v, previous_epoch) or (is_slashed_attester(v) and previous_epoch + 1 < v.withdrawable_epoch) + ] +``` + #### `get_ptc` ```python @@ -497,6 +639,196 @@ def get_indexed_payload_attestation(state: BeaconState, ) ``` +#### `get_validator_excess_balance` + +```python +def get_validator_excess_balance(validator: Validator, balance: Gwei) -> Gwei: + if has_eth1_withdrawal_credential(validator) and balance > MIN_ACTIVATION_BALANCE: + return balance - MIN_ACTIVATION_BALANCE + if is_builder(validator) and balance > MAX_EFFECTIVE_BALANCE: + return balance - MAX_EFFECTIVE_BALANCE + return Gwei(0) +``` + +#### Modified `get_validator_churn_limit` + +```python +def get_validator_churn_limit(state: BeaconState) -> Gwei: + """ + Return the validator churn limit for the current epoch. + """ + churn = max(MIN_PER_EPOCH_CHURN_LIMIT * MIN_ACTIVATION_BALANCE, get_total_active_balance(state) // CHURN_LIMIT_QUOTIENT) + return churn - churn % EFFECTIVE_BALANCE_INCREMENT +``` + +#### Modified `get_expected_withdrawals` +**Note:** the function `get_expected_withdrawals` is modified to churn the withdrawals by balance because of the increase in `MAX_EFFECTIVE_BALANCE` + +```python +def get_expected_withdrawals(state: BeaconState) -> Sequence[Withdrawal]: + epoch = get_current_epoch(state) + withdrawal_index = state.next_withdrawal_index + validator_index = state.next_withdrawal_validator_index + withdrawals: List[Withdrawal] = [] + consumed = 0 + for withdrawal in state.pending_partial_withdrawals: + if withdrawal.withdrawable_epoch > epoch or len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD // 2: + break + validator = state.validators[withdrawal.index] + if validator.exit_epoch == FAR_FUTURE_EPOCH and state.balances[withdrawal.index] > MIN_ACTIVATION_BALANCEa: + withdrawble_balance = min(state.balances[withdrawal.index] - MIN_ACTIVATION_BALANCE, withdrawal.amount) + withdrawals.append(Withdrawal( + index=withdrawal_index, + validator_index=withdrawal.index, + address=ExecutionAddress(validator.withdrawal_credentials[12:]), + amount=withdrawable_balance, + )) + withdrawal_index += WithdrawalIndex(1) + consumed += 1 + state.pending_partial_withdrawals = state.pending_partial_withdrawals[consumed:] + + # Sweep for remaining + bound = min(len(state.validators), MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) + for _ in range(bound): + validator = state.validators[validator_index] + balance = state.balances[validator_index] + if is_fully_withdrawable_validator(validator, balance, epoch): + withdrawals.append(Withdrawal( + index=withdrawal_index, + validator_index=validator_index, + address=ExecutionAddress(validator.withdrawal_credentials[12:]), + amount=balance, + )) + withdrawal_index += WithdrawalIndex(1) + elif is_partially_withdrawable_validator(validator, balance): + withdrawals.append(Withdrawal( + index=withdrawal_index, + validator_index=validator_index, + address=ExecutionAddress(validator.withdrawal_credentials[12:]), + amount=get_validator_excess_balance(validator, balance), + )) + withdrawal_index += WithdrawalIndex(1) + if len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD: + break + validator_index = ValidatorIndex((validator_index + 1) % len(state.validators)) + return withdrawals +``` + +### Beacon state mutators + +#### `compute_exit_epoch_and_update_churn` + +```python +def compute_exit_epoch_and_update_churn(state: BeaconState, exit_balance: Gwei) -> Epoch: + earliest_exit_epoch = compute_activation_exit_epoch(get_current_epoch(state)) + per_epoch_churn = get_validator_churn_limit(state) + # New epoch for exits. + if state.earliest_exit_epoch < earliest_exit_epoch: + state.earliest_exit_epoch = earliest_exit_epoch + state.exit_balance_to_consume = per_epoch_churn + + # Exit fits in the current earliest epoch. + if exit_balance < state.exit_balance_to_consume: + state.exit_balance_to_consume -= exit_balance + else: # Exit doesn't fit in the current earliest epoch. + balance_to_process = exit_balance - state.exit_balance_to_consume + additional_epochs, remainder = divmod(balance_to_process, per_epoch_churn) + state.earliest_exit_epoch += additional_epochs + state.exit_balance_to_consume = per_epoch_churn - remainder + return state.earliest_exit_epoch +``` + +#### Modified `initiate_validator_exit` +**Note:** the function `initiate_validator_exit` is modified to use the new churn mechanism. + +```python +def initiate_validator_exit(state: BeaconState, index: ValidatorIndex) -> None: + """ + Initiate the exit of the validator with index ``index``. + """ + # Return if validator already initiated exit + validator = state.validators[index] + if validator.exit_epoch != FAR_FUTURE_EPOCH: + return + + # Compute exit queue epoch + exit_queue_epoch = compute_exit_epoch_and_update_churn(state, state.balances[index]) + + # Set validator exit epoch and withdrawable epoch + validator.exit_epoch = exit_queue_epoch + validator.withdrawable_epoch = Epoch(validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY) +``` + +#### Modified `slash_validator` +**Note:** The function `slash_validator` is modified to use the new flag system. + +```python +def slash_validator(state: BeaconState, + slashed_index: ValidatorIndex, + whistleblower_index: ValidatorIndex=None) -> None: + """ + Slash the validator with index ``slashed_index``. + """ + epoch = get_current_epoch(state) + initiate_validator_exit(state, slashed_index) + validator = state.validators[slashed_index] + validator.slashed = add_flag(validator.slashed, SLASHED_ATTESTER_FLAG_INDEX) + validator.withdrawable_epoch = max(validator.withdrawable_epoch, Epoch(epoch + EPOCHS_PER_SLASHINGS_VECTOR)) + state.slashings[epoch % EPOCHS_PER_SLASHINGS_VECTOR] += validator.effective_balance + decrease_balance(state, slashed_index, validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT) + + # Apply proposer and whistleblower rewards + proposer_index = get_beacon_proposer_index(state) + if whistleblower_index is None: + whistleblower_index = proposer_index + whistleblower_reward = Gwei(validator.effective_balance // WHISTLEBLOWER_REWARD_QUOTIENT) + proposer_reward = Gwei(whistleblower_reward // PROPOSER_REWARD_QUOTIENT) + increase_balance(state, proposer_index, proposer_reward) + increase_balance(state, whistleblower_index, Gwei(whistleblower_reward - proposer_reward)) +``` + +## Genesis + +### Modified `initialize_beacon_statre_from_eth1` + +```python +def initialize_beacon_state_from_eth1(eth1_block_hash: Hash32, + eth1_timestamp: uint64, + deposits: Sequence[Deposit]) -> BeaconState: + fork = Fork( + previous_version=GENESIS_FORK_VERSION, + current_version=GENESIS_FORK_VERSION, + epoch=GENESIS_EPOCH, + ) + state = BeaconState( + genesis_time=eth1_timestamp + GENESIS_DELAY, + fork=fork, + eth1_data=Eth1Data(block_hash=eth1_block_hash, deposit_count=uint64(len(deposits))), + latest_block_header=BeaconBlockHeader(body_root=hash_tree_root(BeaconBlockBody())), + randao_mixes=[eth1_block_hash] * EPOCHS_PER_HISTORICAL_VECTOR, # Seed RANDAO with Eth1 entropy + ) + + # Process deposits + leaves = list(map(lambda deposit: deposit.data, deposits)) + for index, deposit in enumerate(deposits): + deposit_data_list = List[DepositData, 2**DEPOSIT_CONTRACT_TREE_DEPTH](*leaves[:index + 1]) + state.eth1_data.deposit_root = hash_tree_root(deposit_data_list) + process_deposit(state, deposit) + + # Process activations + for index, validator in enumerate(state.validators): + balance = state.balances[index] + validator.effective_balance = min(balance - balance % EFFECTIVE_BALANCE_INCREMENT, MAX_EFFECTIVE_BALANCE) + if validator.effective_balance >= MIN_ACTIVATION_BALANCE: + validator.activation_eligibility_epoch = GENESIS_EPOCH + validator.activation_epoch = GENESIS_EPOCH + + # Set genesis validators root for domain separation and chain versioning + state.genesis_validators_root = hash_tree_root(state.validators) + + return state +``` + ## 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. @@ -514,11 +846,12 @@ def process_epoch(state: BeaconState) -> None: process_justification_and_finalization(state) process_inactivity_updates(state) process_rewards_and_penalties(state) - process_registry_updates(state) + process_registry_updates(state) # [Modified in ePBS] process_slashings(state) process_eth1_data_reset(state) - process_effective_balance_updates(state) - process_slashings_reset(state) + process_pending_balance_deposits(state) # [New in ePBS] + process_effective_balance_updates(state) # [Modified in ePBS] + process_slashings_reset(state) # [Modified in ePBS] process_randao_mixes_reset(state) process_historical_summaries_update(state) process_participation_flag_updates(state) @@ -526,6 +859,99 @@ def process_epoch(state: BeaconState) -> None: process_builder_updates(state) # [New in ePBS] ``` +#### Helper functions + +##### Modified `process_registry_updates` + +```python +def process_registry_updates(state: BeaconState) -> None: + # Process activation eligibility and ejections + for index, validator in enumerate(state.validators): + if is_eligible_for_activation_queue(validator): + validator.activation_eligibility_epoch = get_current_epoch(state) + 1 + + if ( + is_active_validator(validator, get_current_epoch(state)) + and validator.effective_balance <= EJECTION_BALANCE + ): + initiate_validator_exit(state, ValidatorIndex(index)) + + # Activate all eligible validators + activation_epoch = compute_activation_exit_epoch(get_current_epoch(state)) + for validator in state.validators: + if is_eligible_for_activation(state, validator): + validator.activation_epoch = activation_epoch +``` + +##### `process_pending_balance_deposits` + +```python +def process_pending_balance_deposits(state: BeaconState) -> None: + state.deposit_balance_to_consume += get_validator_churn_limit(state) + next_pending_deposit_index = 0 + for pending_balance_deposit in state.pending_balance_deposits: + if state.deposit_balance_to_consume < pending_balance_deposit.amount: + break + + state.deposit_balance_to_consume -= pending_balance_deposit.amount + increase_balance(state, pending_balance_deposit.index, pending_balance_deposit.amount) + next_pending_deposit_index += 1 + + state.pending_balance_deposits = state.pending_balance_deposits[next_pending_deposit_index:] +``` + +##### Modified `process_effective_balance_updates` + +```python +def process_effective_balance_updates(state: BeaconState) -> None: + # Update effective balances with hysteresis + for index, validator in enumerate(state.validators): + balance = state.balances[index] + HYSTERESIS_INCREMENT = uint64(EFFECTIVE_BALANCE_INCREMENT // HYSTERESIS_QUOTIENT) + DOWNWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_DOWNWARD_MULTIPLIER + UPWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_UPWARD_MULTIPLIER + EFFECTIVE_BALANCE_LIMIT = MAX_EFFECTIVE_BALANCE if is_builder(validator) else MIN_ACTIVATION_BALANCE + if ( + balance + DOWNWARD_THRESHOLD < validator.effective_balance + or validator.effective_balance + UPWARD_THRESHOLD < balance + ): + validator.effective_balance = min(balance - balance % EFFECTIVE_BALANCE_INCREMENT, EFFECTIVE_BALANCE_LIMIT) +``` + +##### Modified `process_slashings` +**Note:** The only modification is to use the new flag mechanism + +```python +def process_slashings(state: BeaconState) -> None: + epoch = get_current_epoch(state) + total_balance = get_total_active_balance(state) + adjusted_total_slashing_balance = min(sum(state.slashings) * PROPORTIONAL_SLASHING_MULTIPLIER, total_balance) + for index, validator in enumerate(state.validators): + if is_slashed_attester(validator) and epoch + EPOCHS_PER_SLASHINGS_VECTOR // 2 == validator.withdrawable_epoch: + increment = EFFECTIVE_BALANCE_INCREMENT # Factored out from penalty numerator to avoid uint64 overflow + penalty_numerator = validator.effective_balance // increment * adjusted_total_slashing_balance + penalty = penalty_numerator // total_balance * increment + decrease_balance(state, ValidatorIndex(index), penalty) +``` + +##### Modified `get_unslashed_attesting_indices` +**Note:** The function `get_unslashed_attesting_indices` is modified to return only the attester slashing validators. + +```python +def get_unslashed_participating_indices(state: BeaconState, flag_index: int, epoch: Epoch) -> Set[ValidatorIndex]: + """ + Return the set of validator indices that are both active and unslashed for the given ``flag_index`` and ``epoch``. + """ + assert epoch in (get_previous_epoch(state), get_current_epoch(state)) + if epoch == get_current_epoch(state): + epoch_participation = state.current_epoch_participation + else: + epoch_participation = state.previous_epoch_participation + active_validator_indices = get_active_validator_indices(state, epoch) + participating_indices = [i for i in active_validator_indices if has_flag(epoch_participation[i], flag_index)] + return set(filter(lambda index: not is_slashed_attester(state.validators[index]), participating_indices)) +``` + ### Execution engine #### Request data @@ -563,8 +989,8 @@ def notify_new_inclusion_list(self: ExecutionEngine, ```python def process_block(state: BeaconState, block: BeaconBlock) -> None: - process_block_header(state, block) - process_withdrawals(state) [Modified in ePBS] + process_block_header(state, block) # [Modified in ePBS] + 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) @@ -572,6 +998,33 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None: process_sync_aggregate(state, block.body.sync_aggregate) ``` +#### Modified `process_block_header` +**Note:** the only modification is in the `slashed` verification. + +```python +def process_block_header(state: BeaconState, block: BeaconBlock) -> None: + # Verify that the slots match + assert block.slot == state.slot + # Verify that the block is newer than latest block header + assert block.slot > state.latest_block_header.slot + # Verify that proposer index is the correct index + assert block.proposer_index == get_beacon_proposer_index(state) + # Verify that the parent matches + assert block.parent_root == hash_tree_root(state.latest_block_header) + # Cache current block as the new latest block + state.latest_block_header = BeaconBlockHeader( + slot=block.slot, + proposer_index=block.proposer_index, + parent_root=block.parent_root, + state_root=Bytes32(), # Overwritten in the next process_slot call + body_root=hash_tree_root(block.body), + ) + + # Verify proposer is not slashed + proposer = state.validators[block.proposer_index] + assert proposer.slashed == uint8(0) +``` + #### Modified `process_operations` **Note:** `process_operations` is modified to process PTC attestations @@ -585,16 +1038,71 @@ def process_operations(state: BeaconState, body: BeaconBlockBody) -> 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.proposer_slashings, process_proposer_slashing) # [Modified in ePBS] + for_ops(body.attester_slashings, process_attester_slashing) # [Modified in ePBS] for_ops(body.attestations, process_attestation) 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] + for_ops(body.execution_payload_withdraw_request, process_execution_layer_withdraw_request) # [New in ePBS] ``` -#### Modified `process_attestation` +##### Modified Proposer slashings + +```python +def process_proposer_slashing(state: BeaconState, proposer_slashing: ProposerSlashing) -> None: + header_1 = proposer_slashing.signed_header_1.message + header_2 = proposer_slashing.signed_header_2.message + + # Verify header slots match + assert header_1.slot == header_2.slot + # Verify header proposer indices match + assert header_1.proposer_index == header_2.proposer_index + # Verify the headers are different + assert header_1 != header_2 + # Verify the proposer is slashable + proposer = state.validators[header_1.proposer_index] + assert proposer.activation_epoch <= get_current_epoch(state) and not is_slashed_proposer(proposer) + # Verify signatures + for signed_header in (proposer_slashing.signed_header_1, proposer_slashing.signed_header_2): + domain = get_domain(state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(signed_header.message.slot)) + signing_root = compute_signing_root(signed_header.message, domain) + assert bls.Verify(proposer.pubkey, signing_root, signed_header.signature) + + # Apply penalty + penalty = PROPOSER_EQUIVOCATION_PENALTY_FACTOR * EFFECTIVE_BALANCE_INCREMENT + decrease_balance(state, header_1.proposer_index, penalty) + initiate_validator_exit(state, header_1.proposer_index) + proposer.slashed = add_flag(proposer.slashed, SLASHED_PROPOSER_FLAG_INDEX) + + # Apply proposer and whistleblower rewards + proposer_reward = Gwei((penalty // WHISTLEBLOWER_REWARD_QUOTIENT) * PROPOSER_WEIGHT // WEIGHT_DENOMINATOR) + increase_balance(state, get_beacon_proposer_index(state), proposer_reward) +``` + +##### Modified Attester slashings +**Note:** The only modification is the use of `is_attester_slashable_validator` + +```python +def process_attester_slashing(state: BeaconState, attester_slashing: AttesterSlashing) -> None: + attestation_1 = attester_slashing.attestation_1 + attestation_2 = attester_slashing.attestation_2 + assert is_slashable_attestation_data(attestation_1.data, attestation_2.data) + assert is_valid_indexed_attestation(state, attestation_1) + assert is_valid_indexed_attestation(state, attestation_2) + + slashed_any = False + indices = set(attestation_1.attesting_indices).intersection(attestation_2.attesting_indices) + for index in sorted(indices): + if is_attester_slashable_validator(state.validators[index], get_current_epoch(state)): + slash_validator(state, index) + slashed_any = True + assert slashed_any +``` + + +##### Modified `process_attestation` *Note*: The function `process_attestation` is modified to ignore attestations from the ptc @@ -636,6 +1144,51 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None: increase_balance(state, get_beacon_proposer_index(state), proposer_reward) ``` +##### Modified `get_validator_from_deposit` +**Note:** The function `get_validator_from_deposit` is modified to take only a pubkey and withdrawal credentials and sets the effective balance to zero + +```python +def get_validator_from_deposit(pubkey: BLSPubkey, withdrawal_credentials: Bytes32) -> Validator: + return Validator( + pubkey=pubkey, + withdrawal_credentials=withdrawal_credentials, + activation_eligibility_epoch=FAR_FUTURE_EPOCH, + activation_epoch=FAR_FUTURE_EPOCH, + exit_epoch=FAR_FUTURE_EPOCH, + withdrawable_epoch=FAR_FUTURE_EPOCH, + effective_balance=0, + ) +``` + +##### Modified `apply_deposit` + +```python +def apply_deposit(state: BeaconState, + pubkey: BLSPubkey, + withdrawal_credentials: Bytes32, + amount: uint64, + signature: BLSSignature) -> None: + validator_pubkeys = [v.pubkey for v in state.validators] + if pubkey not in validator_pubkeys: + # Verify the deposit signature (proof of possession) which is not checked by the deposit contract + deposit_message = DepositMessage( + pubkey=pubkey, + withdrawal_credentials=withdrawal_credentials, + amount=amount, + ) + domain = compute_domain(DOMAIN_DEPOSIT) # Fork-agnostic domain since deposits are valid across forks + signing_root = compute_signing_root(deposit_message, domain) + if bls.Verify(pubkey, signing_root, signature): + index = len(state.validators) + state.validators.append(get_validator_from_deposit(pubkey, withdrawal_credentials)) + state.balances.append(0) + state.previous_epoch_participation.append(ParticipationFlags(0b0000_0000)) + state.current_epoch_participation.append(ParticipationFlags(0b0000_0000)) + state.inactivity_scores.append(uint64(0)) + else: + index = ValidatorIndex(validator_pubkeys.index(pubkey)) + state.pending_balance_deposits.append(PendingBalanceDeposit(index=index, amount=amount)) +``` ##### Payload Attestations @@ -677,8 +1230,44 @@ def process_payload_attestation(state: BeaconState, payload_attestation: Payload increase_balance(state, get_beacon_proposer_index(state), proposer_reward) ``` +##### Execution Layer Withdraw Requests + +```python +def process_execution_layer_withdraw_request( + state: BeaconState, + execution_layer_withdraw_request: ExecutionLayerWithdrawRequest + ) -> None: + validator_pubkeys = [v.pubkey for v in state.validators] + validator_index = ValidatorIndex(validator_pubkeys.index(execution_layer_withdraw_request.validator_pubkey)) + validator = state.validators[validator_index] + + # Same conditions as in EIP7002 https://github.com/ethereum/consensus-specs/pull/3349/files#diff-7a6e2ba480d22d8bd035bd88ca91358456caf9d7c2d48a74e1e900fe63d5c4f8R223 + # Verify withdrawal credentials + assert validator.withdrawal_credentials[:1] == ETH1_ADDRESS_WITHDRAWAL_PREFIX + assert validator.withdrawal_credentials[12:] == execution_layer_withdraw_request.source_address + assert is_active_validator(validator, get_current_epoch(state)) + # Verify exit has not been initiated, and slashed + assert validator.exit_epoch == FAR_FUTURE_EPOCH: + # Verify the validator has been active long enough + assert get_current_epoch(state) >= validator.activation_epoch + config.SHARD_COMMITTEE_PERIOD + + pending_balance_to_withdraw = sum(item.amount for item in state.pending_partial_withdrawals if item.index == validator_index) + + available_balance = state.balances[validator_index] - MIN_ACTIVATION_BALANCE - pending_balance_to_withdraw + assert available_balance >= execution_layer_withdraw_request.balance + + exit_queue_epoch = compute_exit_epoch_and_update_churn(state, available_balance) + withdrawable_epoch = Epoch(exit_queue_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY) + + state.pending_partial_withdrawals.append(PartialWithdrawal( + index=validator_index, + amount=available_balance, + withdrawable_epoch=withdrawable_epoch, + )) +``` + #### Modified `process_withdrawals` -**Note:** TODO: This is modified to take only the State as parameter as they are deterministic. Still need to include the MaxEB changes +**Note:** TODO: This is modified to take only the State as parameter as they are deterministic. ```python def process_withdrawals(state: BeaconState) -> None: diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index 7ee22d7a4e..743165325d 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -127,3 +127,6 @@ There is no need to do anything about proposer equivocations. Builders should re - If the builder reveals, he knows that he can never be unbundled unless the next committee has a majority of malicious validators: attestations will go for an empty block before a block that is revealed after 8 seconds. - So since the builder cannot be unbundled, then he either doesn't pay if the block is not included, or pays and its included. - The splitting grief, that is, the proposer's block has about 50% of the vote at 8 seconds, remains. + +## Increased Max EB +This PR includes the changes from [this PR](https://github.com/michaelneuder/consensus-specs/pull/3). In particular it includes execution layer triggerable withdrawals. diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index e0b25c321d..ba0d6ad93a 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -207,7 +207,7 @@ def get_weight(store: Store, root: Root, is_payload_present: bool) -> 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 state.validators[i].slashed + if not is_slashed_attester(state.validators[i]) ] attestation_score = Gwei(sum( state.validators[i].effective_balance for i in unslashed_and_active_indices diff --git a/specs/_features/epbs/weak-subjectivity.md b/specs/_features/epbs/weak-subjectivity.md new file mode 100644 index 0000000000..6f79bfbfcd --- /dev/null +++ b/specs/_features/epbs/weak-subjectivity.md @@ -0,0 +1,52 @@ +# ePBS -- Weak Subjectivity Guide + +## Table of contents + + + + + + + +## 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 +``` + + From a29a34c6db92559c1239f350028680d0468c2196 Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 4 Sep 2023 18:09:11 -0300 Subject: [PATCH 040/112] add el withdraws in the beacon block body --- specs/_features/epbs/beacon-chain.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index c57150cb19..5c80a08a17 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -155,6 +155,7 @@ For a further introduction please refer to this [ethresear.ch article](https://e | Name | Value | | - | - | | `MAX_PAYLOAD_ATTESTATIONS` | `2**1` (= 2) # (New in ePBS) | +| `MAX_EXECUTION_LAYER_WITHDRAW_REQUESTS` | `2**4` (= 16) # (New in ePBS) | ### Incentivization weights @@ -343,6 +344,8 @@ class BeaconBlockBody(Container): # PBS signed_execution_payload_header_envelope: SignedExecutionPayloadHeaderEnvelope # [New in ePBS] payload_attestations: List[PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS] # [New in ePBS] + execution_payload_withdraw_requests: List[ExecutionLayerWithdrawRequest, MAX_EXECUTION_LAYER_WITHDRAW_REQUESTS] # [New in ePBS] + ``` #### `ExecutionPayload` @@ -1045,7 +1048,7 @@ def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: 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] - for_ops(body.execution_payload_withdraw_request, process_execution_layer_withdraw_request) # [New in ePBS] + for_ops(body.execution_payload_withdraw_requests, process_execution_layer_withdraw_request) # [New in ePBS] ``` ##### Modified Proposer slashings From faacffe41868a6511b1b97c7162d2e6fd0f42c3c Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 4 Sep 2023 18:18:16 -0300 Subject: [PATCH 041/112] doctoc --- specs/_features/epbs/beacon-chain.md | 37 +++++++++++++++++++++-- specs/_features/epbs/design.md | 2 ++ specs/_features/epbs/weak-subjectivity.md | 4 +++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 5c80a08a17..8dbe69a059 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -9,6 +9,7 @@ - [Introduction](#introduction) - [Constants](#constants) - [Withdrawal prefixes](#withdrawal-prefixes) + - [Slashing flags](#slashing-flags) - [Configuration](#configuration) - [Time parameters](#time-parameters) - [Preset](#preset) @@ -16,17 +17,20 @@ - [Domain types](#domain-types) - [Gwei values](#gwei-values) - [Time parameters](#time-parameters-1) + - [State list lenghts](#state-list-lenghts) - [Rewards and penalties](#rewards-and-penalties) - [Max operations per block](#max-operations-per-block) - [Incentivization weights](#incentivization-weights) - [Execution](#execution) - [Containers](#containers) - [New containers](#new-containers) + - [`PendingBalanceDeposit`](#pendingbalancedeposit) + - [`PartialWithdrawal`](#partialwithdrawal) + - [`ExecutionLayerWithdrawalRequest`](#executionlayerwithdrawalrequest) - [`PayloadAttestationData`](#payloadattestationdata) - [`PayloadAttestation`](#payloadattestation) - [`PayloadAttestationMessage`](#payloadattestationmessage) - [`IndexedPayloadAttestation`](#indexedpayloadattestation) - - [`SignedExecutionPayloadHeader`](#signedexecutionpayloadheader) - [`ExecutionPayloadHeaderEnvelope`](#executionpayloadheaderenvelope) - [`SignedExecutionPayloadHeaderEnvelope`](#signedexecutionpayloadheaderenvelope) - [`ExecutionPayloadEnvelope`](#executionpayloadenvelope) @@ -36,6 +40,7 @@ - [`SignedInclusionListSummary`](#signedinclusionlistsummary) - [`InclusionList`](#inclusionlist) - [Modified containers](#modified-containers) + - [`Validator`](#validator) - [`BeaconBlockBody`](#beaconblockbody) - [`ExecutionPayload`](#executionpayload) - [`ExecutionPayloadHeader`](#executionpayloadheader) @@ -45,23 +50,51 @@ - [`bit_floor`](#bit_floor) - [Predicates](#predicates) - [`is_builder`](#is_builder) + - [`is_eligible_for_activation_queue`](#is_eligible_for_activation_queue) + - [`is_slashed_proposer`](#is_slashed_proposer) + - [`is_slashed_attester`](#is_slashed_attester) + - [Modified `is_slashable_validator`](#modified-is_slashable_validator) + - [Modified `is_fully_withdrawable_validator`](#modified-is_fully_withdrawable_validator) + - [`is_partially_withdrawable_validator`](#is_partially_withdrawable_validator) - [`is_valid_indexed_payload_attestation`](#is_valid_indexed_payload_attestation) - [Beacon State accessors](#beacon-state-accessors) + - [Modified `get_eligible_validator_indices`](#modified-get_eligible_validator_indices) - [`get_ptc`](#get_ptc) - [`get_payload_attesting_indices`](#get_payload_attesting_indices) - [`get_indexed_payload_attestation`](#get_indexed_payload_attestation) + - [`get_validator_excess_balance`](#get_validator_excess_balance) + - [Modified `get_validator_churn_limit`](#modified-get_validator_churn_limit) + - [Modified `get_expected_withdrawals`](#modified-get_expected_withdrawals) + - [Beacon state mutators](#beacon-state-mutators) + - [`compute_exit_epoch_and_update_churn`](#compute_exit_epoch_and_update_churn) + - [Modified `initiate_validator_exit`](#modified-initiate_validator_exit) + - [Modified `slash_validator`](#modified-slash_validator) +- [Genesis](#genesis) + - [Modified `initialize_beacon_statre_from_eth1`](#modified--initialize_beacon_statre_from_eth1) - [Beacon chain state transition function](#beacon-chain-state-transition-function) - [Epoch processing](#epoch-processing) - [Modified `process_epoch`](#modified-process_epoch) + - [Helper functions](#helper-functions-1) + - [Modified `process_registry_updates`](#modified-process_registry_updates) + - [`process_pending_balance_deposits`](#process_pending_balance_deposits) + - [Modified `process_effective_balance_updates`](#modified-process_effective_balance_updates) + - [Modified `process_slashings`](#modified-process_slashings) + - [Modified `get_unslashed_attesting_indices`](#modified-get_unslashed_attesting_indices) - [Execution engine](#execution-engine) - [Request data](#request-data) - [New `NewInclusionListRequest`](#new-newinclusionlistrequest) - [Engine APIs](#engine-apis) - [New `notify_new_inclusion_list`](#new-notify_new_inclusion_list) - [Block processing](#block-processing) + - [Modified `process_block_header`](#modified-process_block_header) - [Modified `process_operations`](#modified-process_operations) - - [Modified `process_attestation`](#modified-process_attestation) + - [Modified Proposer slashings](#modified-proposer-slashings) + - [Modified Attester slashings](#modified-attester-slashings) + - [Modified `process_attestation`](#modified-process_attestation) + - [Modified `get_validator_from_deposit`](#modified-get_validator_from_deposit) + - [Modified `apply_deposit`](#modified-apply_deposit) - [Payload Attestations](#payload-attestations) + - [Execution Layer Withdraw Requests](#execution-layer-withdraw-requests) - [Modified `process_withdrawals`](#modified-process_withdrawals) - [New `verify_execution_payload_header_envelope_signature`](#new-verify_execution_payload_header_envelope_signature) - [New `process_execution_payload_header`](#new-process_execution_payload_header) diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index 743165325d..cb885ae438 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -11,6 +11,8 @@ - [PTC Rewards](#ptc-rewards) - [PTC Attestations](#ptc-attestations) - [Forkchoice changes](#forkchoice-changes) + - [Equivocations](#equivocations) + - [Increased Max EB](#increased-max-eb) diff --git a/specs/_features/epbs/weak-subjectivity.md b/specs/_features/epbs/weak-subjectivity.md index 6f79bfbfcd..19bd08068b 100644 --- a/specs/_features/epbs/weak-subjectivity.md +++ b/specs/_features/epbs/weak-subjectivity.md @@ -6,6 +6,10 @@ +- [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 From aa88419677e2f6bbc7e31f9f5479abe651dfe9b5 Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 8 Sep 2023 14:34:36 -0300 Subject: [PATCH 042/112] add slot to IL --- specs/_features/epbs/beacon-chain.md | 1 + 1 file changed, 1 insertion(+) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 8dbe69a059..20b6ec4b4a 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -333,6 +333,7 @@ class SignedInclusionListSummary(Container): ```python class InclusionList(Container) summary: SignedInclusionListSummary + slot: Slot transactions: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] ``` From 66f8ee11ccc18da7a4105370f035b5162ac1e0a7 Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 11 Sep 2023 08:17:16 -0300 Subject: [PATCH 043/112] use head_root in get_head --- specs/_features/epbs/fork-choice.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index ba0d6ad93a..788e759e15 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -251,7 +251,8 @@ def get_head(store: Store) -> tuple[Root, bool]: # Ties broken by favoring block with lexicographically higher root # Ties then broken by favoring full blocks # TODO: Can (root, full), (root, empty) have the same weight? - head = max(children, key=lambda (root, present): (get_weight(store, root, present), root, present)) + head_root = max(children, key=lambda (root, present): (get_weight(store, root, present), root, present)) + head_full = is_payload_present(store, head_root) ``` ## Updated fork-choice handlers From b06e017efe5f85437e3ded5a4a70c6f3f864e8c0 Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 11 Sep 2023 08:27:49 -0300 Subject: [PATCH 044/112] design notes --- specs/_features/epbs/design.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index cb885ae438..098bb9a04b 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -59,6 +59,7 @@ So when importing the CL block for slot N, we process the expected withdrawals a - PTC members are obtained as the first members from each beacon slot committee that are not builders. - attesters are rewarded as a full attestation when they get the right payload presence: that is, if they vote for full (resp. empty) and the payload is included (resp. not included) then they get their participation bits (target, source and head timely) set. Otherwise they get a penalty as a missed attestation. - Attestations to the CL block from these members are just ignored. +- The proposer for slot N+1 must include PTC attestations for slot N. There is no rewards (and therefore no incentive) for the proposer to include attestations that voted incorrectly, perhaps we can simply accept 1 PTC attestation per block instead of the current two. ## PTC Attestations @@ -120,6 +121,9 @@ F ~~~ E ``` In this case all the attesters for `N+1` will be counted depending on the PTC members that voted for `(N+1, Full)`. Assuming honest PTC members, they would have voted for `N` during `N+1` so any CL attesters for `N+1` would be voting for `N+1, Empty` thus only counting for the head in `(N+3, Full)`. +### Checkpoint states +There is no current change in `store.checkpoint_states[root]`. In principle the "checkpoint state" should correspond to either the checkpoint block being full or empty. However, payload status does not change any consensus value for the state at the given time, so it does not matter if we continue using `store.block_states` which corresponds to the "empty" case. + ## Equivocations There is no need to do anything about proposer equivocations. Builders should reveal their block anyway. From c2eacf92d7273ef1a3ee67944bc3175f3f8ba50e Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 11 Sep 2023 08:33:20 -0300 Subject: [PATCH 045/112] onboard builders --- specs/_features/epbs/design.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index 098bb9a04b..f5eb3a7d5b 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -37,7 +37,7 @@ ePBS introduces forward inclusion lists for proposers to guarantee censor resist There is a new entity `Builder` that is a glorified validator (they are simply validators with a different withdrawal prefix `0x0b`) required to have a higher stake and required to sign when producing execution payloads. - Builders are also validators (otherwise their staked capital depreciates). -- We onboard builders by simply turning validators into builders if they achieve the necessary minimum balance (this way we avoid two forks to onboard builders and keep the same deposit flow, avoid builders to skip the entry churn), we change their withdrawal prefix to be distinguished from normal validators. +- There is nothing to be done to onboard builders as we can simply accept validators with the right `0x0b` withdrawal prefix before the fork. They will be builders automatically. We could however onboard builders by simply turning validators into builders if they achieve the necessary minimum balance and change their withdrawal prefix to be distinguished from normal validators at the fork. - We need to include several changes from the [MaxEB PR](https://github.com/michaelneuder/consensus-specs/pull/3) in order to account with builders having an increased balance that would otherwise depreciate. ## Builder Payments From 1c6304ba359a7df7c9c9e2128c68f3242c2bdf79 Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 11 Sep 2023 10:26:46 -0300 Subject: [PATCH 046/112] block-slot is missing --- specs/_features/epbs/design.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index f5eb3a7d5b..356dd57cd1 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -124,6 +124,10 @@ In this case all the attesters for `N+1` will be counted depending on the PTC me ### Checkpoint states There is no current change in `store.checkpoint_states[root]`. In principle the "checkpoint state" should correspond to either the checkpoint block being full or empty. However, payload status does not change any consensus value for the state at the given time, so it does not matter if we continue using `store.block_states` which corresponds to the "empty" case. +### Block slot + +Currently there is no complete implementation of (block, slot) vote: colluding proposers can in principle reveal a late block and base the next block on top of it. TODO: Fix this + ## Equivocations There is no need to do anything about proposer equivocations. Builders should reveal their block anyway. From c43400a0f9d9ce9ad3e6b4b5deb03a14fd038d4b Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 12 Sep 2023 15:03:46 -0300 Subject: [PATCH 047/112] block slot --- specs/_features/epbs/design.md | 49 ++++++++++++++++++++++- specs/_features/epbs/fork-choice.md | 61 +++++++++++++++++++++++------ 2 files changed, 98 insertions(+), 12 deletions(-) diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index 356dd57cd1..8b97fb609b 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -11,6 +11,8 @@ - [PTC Rewards](#ptc-rewards) - [PTC Attestations](#ptc-attestations) - [Forkchoice changes](#forkchoice-changes) + - [Checkpoint states](#checkpoint-states) + - [Block slot](#block-slot) - [Equivocations](#equivocations) - [Increased Max EB](#increased-max-eb) @@ -126,7 +128,29 @@ There is no current change in `store.checkpoint_states[root]`. In principle the ### Block slot -Currently there is no complete implementation of (block, slot) vote: colluding proposers can in principle reveal a late block and base the next block on top of it. TODO: Fix this +Honest validators that vote for a parent block when a block is late, are contributing for this parent block support and are explicitly attesting that the current block is not present. This is taken into account in the new computation of `get_head`. Consider the following situation +```mermaid +graph RL +A[N-1, Full] +B[N, Full] --> A +``` +The block `N` has arrived late and the whole committee sees `A` as head and vote for `N-1`. At the start of `N+1` a call to `get_head` will return `N-1` as head and thus if the proposer of `N+1` is honest it will base its block on `N-1`. Suppose however that the proposer bases his block on top of `N`. Then we see +```mermaid +graph RL +A[N-1, Full] +B[N, Full] --> A +C[N+1, Full] --> B +``` +This block was timely so it gets proposer Boost. The real DAG is +```mermaid +graph RL +A[N-1, Full] +B[N, Full] --> A +C[N+1, Full] --> B +D[N-1, Full] --> A +E[N-1, Full] --> D +``` +And honest validators should still see `N-1` as head. The reason being that at the attestation deadline on `N+1` validators have seen block `N+1` appear, this block is valid and has 40% of a committee vote because of proposer boost. However, the branch for `N-1` has the full committee from `N` that has voted for it, and thus honest validators vote for `N-1` as valid. ## Equivocations @@ -138,5 +162,28 @@ There is no need to do anything about proposer equivocations. Builders should re - So since the builder cannot be unbundled, then he either doesn't pay if the block is not included, or pays and its included. - The splitting grief, that is, the proposer's block has about 50% of the vote at 8 seconds, remains. +A little care has to be taken in the case of colluding proposers for `N` and `N+1`. Consider the example of the [previous section](#block-slot). The malicious proposer of `N` sends an early block to the builder and an equivocation after it has seen the payload. No honest validators will have voted for this equivocation. Suppose $\beta$ is the malicious stake. We have $1 - \beta$ for that votes for the early `N` as head and $\beta$ that will vote for the lately revealed block. Assuming $\beta < 0.5$ we have that the PTC committee will declare the equivocation as empty. The malicious proposer of `N+1` proposes based on the equivocating block `N` including some unbundled transactions. Because of the PTC vote, even the $\beta$ attestations for the equivocating block `N` will not count for `N+1` since it builds on *full* instead of empty. The weight of `N+1` is only given by proposer boost. The weight of the early `N` will be $1 - \beta$ thus beating the malicious `N+1` if $\beta < 0.6$ and thus honest validators will vote for the early `N` that included the builders' payload. However, the early block itself may cause a split view, in this case some attesters may have voted for `N-1` as head! in this situation we would have a DAG like this (we are not considering irrelevant branches) +```mermaid +graph RL +A[N-1, Full] +B[N, Full] --> A +H[N, Full] --> B +F[N', Full] --> A +I[N', Empty] --> A +C[N+1, Full] --> F +D[N-1, Full] --> A +E[N-1, Full] --> D +``` + +When recursing from the children of `N-1` the weights for the three children are as follows (when computing after `N+1` has been revealed and before validators for `N+1` attested) +- (N, Full) has gotten some vote $\gamma \leq 1 - \beta$. +- (N', Full) has zero weight. This is an important point. Proposer boost does not apply to it because even though $N+1$ will get proposer boost, it is based on the wrong `PTC` vote, and thus it does not count towards this node's weight. +- (N', Empty) has $\beta$ maximum. +- (N-1, Full) has $1 - \beta - \gamma$. + +Thus, supposing $\gamma < \beta$ we have that $1 - \beta - \gamma > 1 - 2 \beta > \beta$ as long as $\beta < 1/3$. Thus we are protected from these kinds of attacks from attackers up to 33%. + +Note however that if we were to apply proposer boost to `(N', Full)` then we see that there's a split now between the three possible heads. `N'` has proposer boost giving it $0.4$ so if $\gamma = 0.39$ we get that with $1 - \beta - \gamma < 0.4$ whenever $\beta \geq 0.2$. Thus a 20% attacker that can also split the network, would be able to carry this attack with two consecutive blocks. + ## Increased Max EB This PR includes the changes from [this PR](https://github.com/michaelneuder/consensus-specs/pull/3). In particular it includes execution layer triggerable withdrawals. diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 788e759e15..3aa3ad5b8a 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -8,6 +8,8 @@ - [Introduction](#introduction) - [Constant](#constant) - [Helpers](#helpers) + - [Modified `LatestMessage`](#modified-latestmessage) + - [Modified `update_latest_messages`](#modified-update_latest_messages) - [Modified `Store`](#modified-store) - [`verify_inclusion_list`](#verify_inclusion_list) - [`is_inclusion_list_available`](#is_inclusion_list_available) @@ -39,6 +41,29 @@ This is the modification of the fork choice accompanying the ePBS upgrade. ## 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. @@ -189,21 +214,27 @@ def get_checkpoint_block(store: Store, root: Root, epoch: Epoch) -> Root: ### `is_supporting_vote` ```python -def is_supporting_vote(store: Store, root: Root, is_payload_present: bool, message_root: Root) -> bool: +def is_supporting_vote(store: Store, root: Root, slot: Slot, is_payload_present: bool, message: LatestMessage) -> bool: """ returns whether a vote for ``message_root`` supports the chain containing the beacon block ``root`` with the - payload contents indicated by ``is_payload_present``. + payload contents indicated by ``is_payload_present`` as head during slot ``slot``. """ - (ancestor_root, is_ancestor_full) = get_ancestor(store, message_root, store.blocks[root].slot) + if root == message_root: + # an attestation for a given root always counts for that root regardless if full or empty + return slot <= message.slot + message_block = store.blocks[message_root] + if slot > message_block.slot: + return False + (ancestor_root, is_ancestor_full) = get_ancestor(store, message_root, slot) return (root == ancestor_root) and (is_payload_preset == is_ancestor_full) ``` ### Modified `get_weight` -**Note:** `get_weight` is modified to only count votes for descending chains that support the status of a pair `Root, bool`, where the `bool` indicates if the block was full or not. +**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. ```python -def get_weight(store: Store, root: Root, is_payload_present: bool) -> Gwei: +def get_weight(store: Store, root: Root, slot: Slot, is_payload_present: bool) -> 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)) @@ -213,7 +244,7 @@ def get_weight(store: Store, root: Root, is_payload_present: bool) -> Gwei: 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, root, is_payload_present, store.latest_messages[i].root)) + and is_supporting_vote(store, root, slot, is_payload_present, store.latest_messages[i])) )) if store.proposer_boost_root == Root(): # Return only attestation score if ``proposer_boost_root`` is not set @@ -239,19 +270,27 @@ def get_head(store: Store) -> tuple[Root, bool]: blocks = get_filtered_block_tree(store) # Execute the LMD-GHOST fork choice head_root = store.justified_checkpoint.root + head_block = store.blocks[head_root] + head_slot = head_block.slot head_full = is_payload_present(store, head_root) while True: children = [ - (root, present) for root in blocks.keys() - if blocks[root].parent_root == head_root for present in (True, False) + (root, block.slot, present) for (root, block) in blocks.items() + if block.parent_root == head_root for present in (True, False) ] if len(children) == 0: return (head_root, head_full) + # if we have children we consider the current head advanced as a possible head + children += [(head_root, head_slot + 1, head_full)] # Sort by latest attesting balance with ties broken lexicographically - # Ties broken by favoring block with lexicographically higher root - # Ties then broken by favoring full blocks + # Ties broken by favoring full blocks + # Ties broken then by favoring higher slot numbers + # Ties then broken by favoring block with lexicographically higher root # TODO: Can (root, full), (root, empty) have the same weight? - head_root = max(children, key=lambda (root, present): (get_weight(store, root, present), root, present)) + child_root = max(children, key=lambda (root, slot, present): (get_weight(store, root, slot, present), present, slot, root)) + if child_root == head_root: + return (head_root, head_full) + head_root = child_root head_full = is_payload_present(store, head_root) ``` From 0c645ad4cb458c6f9089c9e9f5699bbb4cb0f8b0 Mon Sep 17 00:00:00 2001 From: Potuz Date: Thu, 14 Sep 2023 13:04:27 -0300 Subject: [PATCH 048/112] typos --- specs/_features/epbs/beacon-chain.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 20b6ec4b4a..3d6c04a17e 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -487,7 +487,7 @@ class BeaconState(Container): # Deep history valid from Capella onwards historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] # PBS - signed_execution_payload_header_envelope: SignedExecutionPayloadHeaderEnvelop # [New in ePBS] + signed_execution_payload_header_envelope: SignedExecutionPayloadHeaderEnvelope # [New in ePBS] last_withdrawals_root: Root # [New in ePBS] deposit_balance_to_consume: Gwei # [New in ePBS] exit_balance_to_consume: Gwei # [New in ePBS] @@ -1391,7 +1391,7 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti assert envelope.beacon_block_root == hash_tree_root(state.latest_block_header) # Verify consistency with the committed header hash = hash_tree_root(payload) - commited_envelope = state.signed_execution_payload_header_envelope.message + committed_envelope = state.signed_execution_payload_header_envelope.message previous_hash = hash_tree_root(committed_envelope.payload) assert hash == previous_hash # Verify consistency with the envelope @@ -1406,7 +1406,7 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti ) ) # Cache the execution payload header - state.latest_execution_payload_header = committed_envelope.payload + state.latest_execution_payload_header = committed_envelope.header # Verify the state root assert envelope.state_root == hash_tree_root(state) ``` From bcc1d5f4df905a6045a73393b9b5c3a592322ffe Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 18 Sep 2023 07:55:55 -0300 Subject: [PATCH 049/112] typos Terence found with chatgpt --- specs/_features/epbs/fork-choice.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 3aa3ad5b8a..598329ae6f 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -141,7 +141,7 @@ def is_inclusion_list_available(state: BeaconState, block: BeaconBlock) -> bool: """ # Verify that the list is empty if the parent consensus block did not contain a payload if state.signed_execution_payload_header_envelope.message.header != state.latest_execution_payload_header: - return true + return True # verify the inclusion list inclusion_list = retrieve_inclusion_list(block.slot, block.proposer_index) @@ -159,7 +159,7 @@ def notify_ptc_messages(store: Store, state: BeaconState, 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) + data=payload_attestation.data, signature= BLSSignature(), is_from_block=true) ``` ### `is_payload_present` @@ -167,12 +167,12 @@ def notify_ptc_messages(store: Store, state: BeaconState, payload_attestations: ```python def is_payload_present(store: Store, beacon_block_root: Root) -> bool: """ - return wether the execution payload for the beacon block with root ``beacon_block_root`` was voted as present + 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 ptc_vote[beacon_block_root].count(True) > PAYLOAD_TIMELY_THRESHOLD + return store.ptc_vote[beacon_block_root].count(True) > PAYLOAD_TIMELY_THRESHOLD ``` ### Modified `get_ancestor` @@ -396,7 +396,7 @@ def on_excecution_payload(store: Store, signed_envelope: SignedExecutionPayloadE process_execution_payload(state, signed_envelope, EXECUTION_ENGINE) #Add new state for this payload to the store - store.execution_payload_states[beacon_block_root] = state + store.execution_payload_states[envelope.beacon_block_root] = state ``` ### `on_payload_attestation_message` From fad1f497bd4e819ee5cf9122132dfcefdc63e64c Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 20 Sep 2023 16:55:45 -0300 Subject: [PATCH 050/112] add helper to get payload hash --- specs/_features/epbs/fork-choice.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 598329ae6f..d77a9fab60 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -20,6 +20,7 @@ - [`is_supporting_vote`](#is_supporting_vote) - [Modified `get_weight`](#modified-get_weight) - [Modified `get_head`](#modified-get_head) + - [New `get_block_hash`](#new-get_block_hash) - [Updated fork-choice handlers](#updated-fork-choice-handlers) - [`on_block`](#on_block) - [New fork-choice handlers](#new-fork-choice-handlers) @@ -294,6 +295,20 @@ def get_head(store: Store) -> tuple[Root, bool]: head_full = is_payload_present(store, head_root) ``` +### New `get_block_hash` + +```python +def get_blockhash(store: Store, root: Root) -> Hash32: + """ + returns the blockHash of the latest execution payload in the chain containing the + beacon block with root ``root`` + """ + # The block is known + if is_payload_present(store, root): + return hash(store.execution_payload_states[root].latest_block_header) + return hash(store.block__states[root].latest_block_header) +``` + ## Updated fork-choice handlers ### `on_block` From 458bb8f5b35e674de99c815dff65869a2a78438c Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 10 Oct 2023 20:45:59 -0300 Subject: [PATCH 051/112] minimal churn for transfers --- specs/_features/epbs/beacon-chain.md | 4 ++-- specs/_features/epbs/design.md | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 3d6c04a17e..94072e6083 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -1350,13 +1350,13 @@ def verify_execution_payload_header_envelope_signature(state: BeaconState, def process_execution_payload_header(state: BeaconState, block: BeaconBlock) -> None: signed_header_envelope = block.body.signed_execution_payload_header_envelope assert verify_execution_payload_header_envelope_signature(state, signed_header_envelope) - # Check that the builder has funds to cover the bid and transfer the funds + # Check that the builder has funds to cover the bid and schedule the funds for transfer envelope = signed_header_envelope.message builder_index = envelope.builder_index amount = envelope.value assert state.balances[builder_index] >= amount: decrease_balance(state, builder_index, amount) - increase_balance(state, block.proposer_index, amount) + state.pending_balance_deposits.append(PendingBalanceDeposit(index=block.proposer_index, amount=amount)) # Verify the withdrawals_root against the state cached ones assert header.withdrawals_root == state.last_withdrawals_root # Verify consistency of the parent hash with respect to the previous execution payload header diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index 8b97fb609b..c60f29f26a 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -15,6 +15,7 @@ - [Block slot](#block-slot) - [Equivocations](#equivocations) - [Increased Max EB](#increased-max-eb) + - [Validator transfers](#validator-transfers) @@ -187,3 +188,6 @@ Note however that if we were to apply proposer boost to `(N', Full)` then we see ## Increased Max EB This PR includes the changes from [this PR](https://github.com/michaelneuder/consensus-specs/pull/3). In particular it includes execution layer triggerable withdrawals. + +## Validator transfers +One of the main problems of the current design is that a builder can transfer arbitrary amounts to proposers by simply paying a large bid. This is dangerous from a forkchoice perspective as it moves weights from one branch to another instantaneously, it may prevent a large penalty in case of slashing, etc. In order to partially mitigate this, we churn the transfer overloading the deposit system of Max EB, that is we append a `PendingBalanceDeposit` object to the beacon state. This churns the increase in the proposer's balance while it discounts immediately the balance of the builder. We may want to revisit this and add also an exit churn and even deal with equivocations on future iterations. From 296a4e860f90a4e3c51f679b188cb619f0430bde Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 11 Oct 2023 12:00:47 -0300 Subject: [PATCH 052/112] add mapping for exclusion --- specs/_features/epbs/beacon-chain.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 94072e6083..867a5e86c7 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -37,6 +37,7 @@ - [`SignedExecutionPayloadEnvelope`](#signedexecutionpayloadenvelope) - [`InclusionListSummaryEntry`](#inclusionlistsummaryentry) - [`InclusionListSummary`](#inclusionlistsummary) + - [`ExclusionListEntry`](#exclusionlistentry) - [`SignedInclusionListSummary`](#signedinclusionlistsummary) - [`InclusionList`](#inclusionlist) - [Modified containers](#modified-containers) @@ -320,6 +321,14 @@ class InclusionListSummary(Container) summary: List[InclusionListSummaryEntry, MAX_TRANSACTIONS_PER_INCLUSION_LIST] ``` +#### `ExclusionListEntry` + +```python +class ExclusionListEntry(Container): + block_index: uint64 + summary_index: uint64 +``` + #### `SignedInclusionListSummary` ```python @@ -408,7 +417,7 @@ class ExecutionPayload(Container): blob_gas_used: uint64 excess_blob_gas: uint64 inclusion_list_summary: SignedInclusionListSummary # [New in ePBS] - inclusion_list_exclusions: List[uint64, MAX_TRANSACTIONS_PER_INCLUSION_LIST] # [New in ePBS] + inclusion_list_exclusions: List[ExclusionListEntry, MAX_TRANSACTIONS_PER_INCLUSION_LIST] # [New in ePBS] ``` #### `ExecutionPayloadHeader` From 9c5e60bb4e98f575d1f230d673ab0fecdf68e6d7 Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 11 Oct 2023 12:23:17 -0300 Subject: [PATCH 053/112] Revert "add mapping for exclusion" This reverts commit 874e4c714905958ec663e3b3b2b6d6881981eb2f. --- specs/_features/epbs/beacon-chain.md | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 867a5e86c7..94072e6083 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -37,7 +37,6 @@ - [`SignedExecutionPayloadEnvelope`](#signedexecutionpayloadenvelope) - [`InclusionListSummaryEntry`](#inclusionlistsummaryentry) - [`InclusionListSummary`](#inclusionlistsummary) - - [`ExclusionListEntry`](#exclusionlistentry) - [`SignedInclusionListSummary`](#signedinclusionlistsummary) - [`InclusionList`](#inclusionlist) - [Modified containers](#modified-containers) @@ -321,14 +320,6 @@ class InclusionListSummary(Container) summary: List[InclusionListSummaryEntry, MAX_TRANSACTIONS_PER_INCLUSION_LIST] ``` -#### `ExclusionListEntry` - -```python -class ExclusionListEntry(Container): - block_index: uint64 - summary_index: uint64 -``` - #### `SignedInclusionListSummary` ```python @@ -417,7 +408,7 @@ class ExecutionPayload(Container): blob_gas_used: uint64 excess_blob_gas: uint64 inclusion_list_summary: SignedInclusionListSummary # [New in ePBS] - inclusion_list_exclusions: List[ExclusionListEntry, MAX_TRANSACTIONS_PER_INCLUSION_LIST] # [New in ePBS] + inclusion_list_exclusions: List[uint64, MAX_TRANSACTIONS_PER_INCLUSION_LIST] # [New in ePBS] ``` #### `ExecutionPayloadHeader` From 545d6575dfa038eab6419829ae5091d8d5419cf2 Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 11 Oct 2023 16:10:51 -0300 Subject: [PATCH 054/112] fix design for exclusion list --- specs/_features/epbs/design.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index c60f29f26a..c1f3ae4025 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -27,9 +27,9 @@ ePBS introduces forward inclusion lists for proposers to guarantee censor resist - Proposer for slot N submits a signed block and in parallel broadcasts pairs of `summaries` and `transactions` to be included at the beginning of slot N+1. `transactions` are just list of transactions that this proposer wants included at the most at the beginning of N+1. `Summaries` are lists consisting on addresses sending those transactions and their gas limits. The summaries are signed, the transactions aren't. An honest proposer is allowed to send many of these pairs that aren't committed to its beacon block so no double proposing slashing is involved. - Validators for slot N will consider the block for validation only if they have seen at least one pair (summary, transactions). They will consider the block invalid if those transactions are not executable at the start of slot N and if they don't have at least 12.5% higher `maxFeePerGas` than the current slot's `maxFeePerGas`. -- The builder for slot N reveals its payload together with a signed summary of the proposer of slot N-1. The payload is considered only valid if the following applies - - Let k >= 0 be the minimum such that tx[0],...,tx[k-1], the first `k` transactions of the payload of slot N, satisfy some entry in the summary and `tx[k]` does not satisfy any entry. - - There exist transactions in the payload for N-1 that satisfy all the remaining entries in the summary. +- The builder for slot N reveals its payload together with a signed summary of the proposer of slot N-1. Along the summary, the builder includes a list of transactions indices (in strictly increasing order) of the previous payload of slot N-1, that satisfy some entry in the signed inclusion list summary. The payload is considered only valid if the following applies + - For each index `i` in the payload's `inclusion_list_exclusions`, check that the ith transaction `tx[i]` of the payload for `N-1` satisfies some transaction `T[i]` of the current inclusion list. Here `T[i]` is the first entry in the payload's inclusion list that is satisfied by `tx[i]`. This `T[i]` is removed from the inclusion list summary. + - The remaining transactions in the inclusion list summary, are all satisfied by the first transactions in the current payload, in increasing order, starting from the first transaction. - The payload is executable, that is, it's valid from the execution layer perspective. **Note:** in the event that the payload for the canonical block in slot N is not revealed, then the summaries and transactions list for slot N-1 remains valid, the honest proposer for slot N+1 is not allowed to submit a new IL and any such message will be ignored. The builder for N+1 still has to satisfy the summary of N-1. If there are k slots in a row that are missing payloads, the next full slot will still need to satisfy the inclusion list for N-1. From bffcc04b319cb6e64003dd6e29b3c1e70ed4837c Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 13 Oct 2023 10:21:23 -0300 Subject: [PATCH 055/112] fix signature verification --- specs/_features/epbs/beacon-chain.md | 36 ++++++++++++++++++++++++---- specs/_features/epbs/fork-choice.md | 13 ++++------ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 94072e6083..5d90a14fe3 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -26,7 +26,7 @@ - [New containers](#new-containers) - [`PendingBalanceDeposit`](#pendingbalancedeposit) - [`PartialWithdrawal`](#partialwithdrawal) - - [`ExecutionLayerWithdrawalRequest`](#executionlayerwithdrawalrequest) + - [`ExecutionLayerWithdrawRequest`](#executionlayerwithdrawrequest) - [`PayloadAttestationData`](#payloadattestationdata) - [`PayloadAttestation`](#payloadattestation) - [`PayloadAttestationMessage`](#payloadattestationmessage) @@ -99,6 +99,7 @@ - [New `verify_execution_payload_header_envelope_signature`](#new-verify_execution_payload_header_envelope_signature) - [New `process_execution_payload_header`](#new-process_execution_payload_header) - [New `verify_execution_payload_signature`](#new-verify_execution_payload_signature) + - [New `verify_inclusion_list_summary_signature`](#new-verify_inclusion_list_summary_signature) - [Modified `process_execution_payload`](#modified-process_execution_payload) @@ -223,10 +224,10 @@ class PartialWithdrawal(Container) withdrawable_epoch: Epoch ``` -#### `ExecutionLayerWithdrawalRequest` +#### `ExecutionLayerWithdrawRequest` ```python -class ExecutionLayerWithdrawalRequest(Container) +class ExecutionLayerWithdrawRequest(Container) source_address: ExecutionAddress validator_pubkey: BLSPubkey balance: Gwei @@ -293,6 +294,8 @@ class ExecutionPayloadEnvelope(Container): builder_index: ValidatorIndex beacon_block_root: Root blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + inclusion_list_proposer_index: ValidatorIndex + inclusion_list_signature: BLSSignature state_root: Root ``` @@ -407,7 +410,7 @@ class ExecutionPayload(Container): withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] blob_gas_used: uint64 excess_blob_gas: uint64 - inclusion_list_summary: SignedInclusionListSummary # [New in ePBS] + inclusion_list_summary: List[InclusionListSummaryEntry, MAX_TRANSACTIONS_PER_INCLUSION_LIST] # [New in ePBS] inclusion_list_exclusions: List[uint64, MAX_TRANSACTIONS_PER_INCLUSION_LIST] # [New in ePBS] ``` @@ -487,6 +490,7 @@ class BeaconState(Container): # Deep history valid from Capella onwards historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] # PBS + latest_execution_payload_proposer: ValidatorIndex # [New in ePBS] signed_execution_payload_header_envelope: SignedExecutionPayloadHeaderEnvelope # [New in ePBS] last_withdrawals_root: Root # [New in ePBS] deposit_balance_to_consume: Gwei # [New in ePBS] @@ -1378,6 +1382,17 @@ def verify_execution_envelope_signature(state: BeaconState, signed_envelope: Sig 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: + # TODO: do we need a new domain? + summary = signed_summary.message + signing_root = compute_signing_root(summary, get_domain(state, DOMAIN_BEACON_PROPOSER)) + proposer = state.validators[message.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. @@ -1387,6 +1402,16 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti assert verify_execution_envelope_signature(state, signed_envelope) envelope = signed_envelope.message payload = envelope.payload + # Verify inclusion list proposer + proposer_index = envelope.inclusion_list_proposer_index + assert proposer_index == state.latest_execution_payload_proposer + # Verify inclusion list summary signature + signed_summary = SignedInclusionListSummary( + message=InclusionListSummary( + proposer_index=proposer_index + 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 @@ -1405,8 +1430,9 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti parent_beacon_block_root=state.latest_block_header.parent_root, ) ) - # Cache the execution payload header + # Cache the execution payload header and proposer state.latest_execution_payload_header = committed_envelope.header + state.latest_execution_payload_proposer = proposer_index # Verify the state root assert envelope.state_root == hash_tree_root(state) ``` diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index d77a9fab60..80eeff1128 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -102,10 +102,7 @@ def verify_inclusion_list(state: BeaconState, block: BeaconBlock, inclusion_list assert block.proposer_index == proposer_index # Check that the signature is correct - # TODO: do we need a new domain? - signing_root = compute_signing_root(signed_summary.message, get_domain(state, DOMAIN_BEACON_PROPOSER)) - proposer = state.validators[proposer_index] - assert bls.Verify(proposer.pubkey, signing_root, signed_summary.signature) + 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 @@ -217,16 +214,16 @@ def get_checkpoint_block(store: Store, root: Root, epoch: Epoch) -> Root: ```python def is_supporting_vote(store: Store, root: Root, slot: Slot, is_payload_present: bool, message: LatestMessage) -> bool: """ - returns whether a vote for ``message_root`` supports the chain containing the beacon block ``root`` with the + returns whether a vote for ``message.root`` supports the chain containing the beacon block ``root`` with the payload contents indicated by ``is_payload_present`` as head during slot ``slot``. """ - if root == message_root: + if root == message.root: # an attestation for a given root always counts for that root regardless if full or empty return slot <= message.slot - message_block = store.blocks[message_root] + message_block = store.blocks[message.root] if slot > message_block.slot: return False - (ancestor_root, is_ancestor_full) = get_ancestor(store, message_root, slot) + (ancestor_root, is_ancestor_full) = get_ancestor(store, message.root, slot) return (root == ancestor_root) and (is_payload_preset == is_ancestor_full) ``` From 99b2109edc2e3c31f70d0f0950499459a19708a6 Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 13 Oct 2023 11:17:25 -0300 Subject: [PATCH 056/112] fix il proposers --- specs/_features/epbs/beacon-chain.md | 18 +++++++++++++++--- specs/_features/epbs/design.md | 6 ++++++ specs/_features/epbs/fork-choice.md | 4 ++-- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 5d90a14fe3..4855997599 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -57,6 +57,7 @@ - [Modified `is_fully_withdrawable_validator`](#modified-is_fully_withdrawable_validator) - [`is_partially_withdrawable_validator`](#is_partially_withdrawable_validator) - [`is_valid_indexed_payload_attestation`](#is_valid_indexed_payload_attestation) + - [`is_parent_block_full`](#is_parent_block_full) - [Beacon State accessors](#beacon-state-accessors) - [Modified `get_eligible_validator_indices`](#modified-get_eligible_validator_indices) - [`get_ptc`](#get_ptc) @@ -490,7 +491,8 @@ class BeaconState(Container): # Deep history valid from Capella onwards historical_summaries: List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT] # PBS - latest_execution_payload_proposer: ValidatorIndex # [New in ePBS] + previous_inclusion_list_proposer: ValidatorIndex # [New in ePBS] + latest_inclusion_list_proposer: ValidatorIndex # [New in ePBS] signed_execution_payload_header_envelope: SignedExecutionPayloadHeaderEnvelope # [New in ePBS] last_withdrawals_root: Root # [New in ePBS] deposit_balance_to_consume: Gwei # [New in ePBS] @@ -617,6 +619,13 @@ def is_valid_indexed_payload_attestation(state: BeaconState, indexed_payload_att return bls.FastAggregateVerify(pubkeys, signing_root, indexed_payload_attestation.signature) ``` +#### `is_parent_block_full` + +```python +def is_parent_block_full(state: BeaconState) -> bool: + return state.signed_execution_payload_header_envelope.message.header == state.latest_execution_payload_header +``` + ### Beacon State accessors #### Modified `get_eligible_validator_indices` @@ -1369,6 +1378,9 @@ def process_execution_payload_header(state: BeaconState, block: BeaconBlock) -> assert header.prev_randao == get_randao_mix(state, get_current_epoch(state)) # Verify timestamp assert header.timestamp == compute_timestamp_at_slot(state, state.slot) + # 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 # Cache execution payload header envelope state.signed_execution_payload_header_envelope = signed_header_envelope ``` @@ -1404,7 +1416,7 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti payload = envelope.payload # Verify inclusion list proposer proposer_index = envelope.inclusion_list_proposer_index - assert proposer_index == state.latest_execution_payload_proposer + assert proposer_index == state.previous_inclusion_list_proposer # Verify inclusion list summary signature signed_summary = SignedInclusionListSummary( message=InclusionListSummary( @@ -1432,7 +1444,7 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti ) # Cache the execution payload header and proposer state.latest_execution_payload_header = committed_envelope.header - state.latest_execution_payload_proposer = proposer_index + state.previous_inclusion_list_proposer = state.latest_inclusion_list_proposer # Verify the state root assert envelope.state_root == hash_tree_root(state) ``` diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index c1f3ae4025..6eecaeee4d 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -4,6 +4,7 @@ - [ePBS design notes](#epbs-design-notes) - [Inclusion lists](#inclusion-lists) + - [Liveness](#liveness) - [Builders](#builders) - [Builder Payments](#builder-payments) - [Withdrawals](#withdrawals) @@ -34,6 +35,11 @@ ePBS introduces forward inclusion lists for proposers to guarantee censor resist **Note:** in the event that the payload for the canonical block in slot N is not revealed, then the summaries and transactions list for slot N-1 remains valid, the honest proposer for slot N+1 is not allowed to submit a new IL and any such message will be ignored. The builder for N+1 still has to satisfy the summary of N-1. If there are k slots in a row that are missing payloads, the next full slot will still need to satisfy the inclusion list for N-1. +### Liveness + +In the usual case of LMD+Ghost we have a proof of the *plausible liveness* theorem, that is that supermajority links can always be added to produce new finalized checkpoints provided there exist children extending the finalized chain. Here we prove that the next builder can always produce a valid payload, in particular, a payload that can satisfy the pending inclusion list. + +Let N be the last slot which contained a full execution payload, and let $N+1,..., N+k$, $k \geq 1$ be slots in the canonical chain, descending from $N$ that were either skipped or are *empty*, that is, the corresponding execution payload has not been revealed or hasn't been included. The consensus block for $N+k$ has been proposed and it is the canonical head. The builder for $N+k$ has to fulfill the inclusion list proposed by $N$. When importing the block $N$, validators have attested for the ## Builders diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 80eeff1128..519613f5a7 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -137,8 +137,8 @@ def is_inclusion_list_available(state: BeaconState, block: BeaconBlock) -> bool: Note: the p2p network does not guarantee sidecar retrieval outside of `MIN_SLOTS_FOR_INCLUSION_LISTS_REQUESTS` """ - # Verify that the list is empty if the parent consensus block did not contain a payload - if state.signed_execution_payload_header_envelope.message.header != state.latest_execution_payload_header: + # Ignore the list if the parent consensus block did not contain a payload + if !is_parent_block_full(state): return True # verify the inclusion list From 5db1f29bef3cb81e63998a0b54d144d32c0e23d8 Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 13 Oct 2023 11:29:36 -0300 Subject: [PATCH 057/112] add censoring description --- specs/_features/epbs/design.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md index 6eecaeee4d..4609400011 100644 --- a/specs/_features/epbs/design.md +++ b/specs/_features/epbs/design.md @@ -5,6 +5,7 @@ - [ePBS design notes](#epbs-design-notes) - [Inclusion lists](#inclusion-lists) - [Liveness](#liveness) + - [Censoring](#censoring) - [Builders](#builders) - [Builder Payments](#builder-payments) - [Withdrawals](#withdrawals) @@ -39,7 +40,11 @@ ePBS introduces forward inclusion lists for proposers to guarantee censor resist In the usual case of LMD+Ghost we have a proof of the *plausible liveness* theorem, that is that supermajority links can always be added to produce new finalized checkpoints provided there exist children extending the finalized chain. Here we prove that the next builder can always produce a valid payload, in particular, a payload that can satisfy the pending inclusion list. -Let N be the last slot which contained a full execution payload, and let $N+1,..., N+k$, $k \geq 1$ be slots in the canonical chain, descending from $N$ that were either skipped or are *empty*, that is, the corresponding execution payload has not been revealed or hasn't been included. The consensus block for $N+k$ has been proposed and it is the canonical head. The builder for $N+k$ has to fulfill the inclusion list proposed by $N$. When importing the block $N$, validators have attested for the +Let N be the last slot which contained a full execution payload, and let $N+1,..., N+k$, $k \geq 1$ be slots in the canonical chain, descending from $N$ that were either skipped or are *empty*, that is, the corresponding execution payload has not been revealed or hasn't been included. The consensus block for $N+k$ has been proposed and it is the canonical head. The builder for $N+k$ has to fulfill the inclusion list proposed by $N$. When importing the block $N$, validators have attested for availability of at least one valid inclusion list. That is, those transactions would be executable on top of the head block at the time. Let $P$ be the execution payload included by the builder of $N$, this is the current head payload. Transactions in the attested inclusion list can *only* be invalid in a child of $P$ if there are transactions in $P$ that have the same source address and gas usage that was below the gas limit in the summary. For any such transaction the builder can add such transaction to the exclusion list and not include it in its payload. If there are remaining transactions in its payload from the same address, the nonce will have to be higher nonce than the transaction that was added in the exclusion list. This process can be repeated until there are no more transactions in the summary from that given address that have been invalidated. + +### Censoring + +We prove the following: the builder cannot force a transaction in the inclusion list to revert due to gas limit usage. A malicious builder that attempts to add in the exclusion list some transactions from an address with high gas limit but low usage, so that the remaining transactions in the summary have lower gas limit and the included transaction with higher gas usage reverts. However, this is impossible since any transaction in the exclusion list has to have lower nonce since it was already included in the previous block. That is, any attempt by the builder of changing the order in which to include transactions in the exclusion list, will result in its payload being invalid, and thus the inclusion lists transactions that haven't been invalidated on N will remain valid for the next block. ## Builders From f8db1ea7db62aea033a6708e285f7a97f3b87339 Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 2 Apr 2024 15:49:23 -0300 Subject: [PATCH 058/112] blockchain.md --- specs/_features/epbs/beacon-chain.md | 952 ++++----------------------- specs/_features/epbs/fork-choice.md | 29 +- 2 files changed, 149 insertions(+), 832 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 4855997599..2214e9ce7d 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -112,36 +112,23 @@ This is the beacon chain specification of the enshrined proposer builder separat *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 as opposed to the current three. Honest validators gather *signed bids* from builders and submit their consensus blocks (a `SigneddBeaconBlock`) 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. +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 as opposed 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 *full* block from a previous slot (eg. the current slot's proposer did not submit its block). +- A block from a previous slot (eg. the current slot's proposer did not submit its block). - An *empty* block from the current slot (eg. 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). -For a further introduction please refer to this [ethresear.ch article](https://ethresear.ch/t/payload-timeliness-committee-ptc-an-epbs-design/16054) +## Constants -## Constants +### Payload status -### Withdrawal prefixes - -| Name | Value | -| - | - | -| `BUILDER_WITHDRAWAL_PREFIX` | `Bytes1('0x0b')` # (New in ePBS) | - -### Slashing flags -| Name | Value | -| - | - | -| `SLASHED_ATTESTER_FLAG_INDEX`| `0` # (New in ePBS)| -| `SLASHED_PROPOSER_FLAG_INDEX`| `1` # (New in ePBS)| - -## Configuration - -### Time parameters - -| Name | Value | Unit | Duration | -| - | - | :-: | :-: | -| `SECONDS_PER_SLOT` | `uint64(16)` | seconds | 16 seconds # (Modified in ePBS) | +| Name | Value | +| - | - | +| `PAYLOAD_ABSENT` | `uint8(0)` | +| `PAYLOAD_PRESENT` | `uint8(1)` | +| `PAYLOAD_WITHHELD` | `uint8(2)` | +| `PAYLOAD_INVALID_STATUS` | `uint8(3)` | ## Preset @@ -158,89 +145,34 @@ For a further introduction please refer to this [ethresear.ch article](https://e | `DOMAIN_BEACON_BUILDER` | `DomainType('0x1B000000')` # (New in ePBS)| | `DOMAIN_PTC_ATTESTER` | `DomainType('0x0C000000')` # (New in ePBS)| -### Gwei values - -| Name | Value | -| - | - | -| `BUILDER_MIN_BALANCE` | `Gwei(2**10 * 10**9)` = (1,024,000,000,000) # (New in ePBS)| -| `MIN_ACTIVATION_BALANCE` | `Gwei(2**5 * 10**9)` (= 32,000,000,000) # (New in ePBS)| -| `EFFECTIVE_BALANCE_INCREMENT` | `Gwei(2**0 * 10**9)` (= 1,000,000,000) # (New in ePBS)| -| `MAX_EFFECTIVE_BALANCE` | `Gwei(2**11 * 10**9)` = (2,048,000,000,000) # (Modified in ePBS) | - ### Time parameters | Name | Value | Unit | Duration | | - | - | :-: | :-: | | `MIN_SLOTS_FOR_INCLUSION_LISTS_REQUESTS` | `uint64(2)` | slots | 32 seconds # (New in ePBS) | -### State list lenghts -| Name | Value | Unit | Duration | -| - | - | :-: | :-: | -| `MAX_PENDING_BALANCE_DEPOSITS` | `uint64(2**20) = 1 048 576` | `PendingBalanceDeposits` | #(New in ePBS) | -| `MAX_PENDING_PARTIAL_WITHDRAWALS` | `uint64(2**20) = 1 048 576` | `PartialWithdrawals` | # (New in ePBS) | - -### Rewards and penalties - -| Name | Value | -| - | - | -| `PROPOSER_EQUIVOCATION_PENALTY_FACTOR` | `uint64(2**2)` (= 4) # (New in ePBS)| - ### Max operations per block | Name | Value | | - | - | -| `MAX_PAYLOAD_ATTESTATIONS` | `2**1` (= 2) # (New in ePBS) | -| `MAX_EXECUTION_LAYER_WITHDRAW_REQUESTS` | `2**4` (= 16) # (New in ePBS) | - -### Incentivization weights - -| Name | Value | -| - | - | -| `PTC_PENALTY_WEIGHT` | `uint64(2)` # (New in ePBS)| +| `MAX_PAYLOAD_ATTESTATIONS` | `2**2` (= 4) # (New in ePBS) | ### Execution | Name | Value | | - | - | -| MAX_TRANSACTIONS_PER_INCLUSION_LIST | `2**4` (=16) # (New in ePBS) | -| MAX_GAS_PER_INCLUSION_LIST | `2**21` (=2,097,152) # (New in ePBS) | +| MAX_TRANSACTIONS_PER_INCLUSION_LIST | `2**10` (=1024) # (New in ePBS) | ## Containers ### New containers -#### `PendingBalanceDeposit` - -```python -class PendingBalanceDeposit(Container): - index: ValidatorIndex - amount: Gwei -``` - -#### `PartialWithdrawal` - -```python -class PartialWithdrawal(Container) - index: ValidatorIndex - amount: Gwei - withdrawable_epoch: Epoch -``` - -#### `ExecutionLayerWithdrawRequest` - -```python -class ExecutionLayerWithdrawRequest(Container) - source_address: ExecutionAddress - validator_pubkey: BLSPubkey - balance: Gwei -``` - #### `PayloadAttestationData` ```python class PayloadAttestationData(Container): beacon_block_root: Root slot: Slot - payload_present: bool + payload_status: uint8 ``` #### `PayloadAttestation` @@ -270,20 +202,11 @@ class IndexedPayloadAttestation(Container): signature: BLSSignature ``` -#### `ExecutionPayloadHeaderEnvelope` - -```python -class ExecutionPayloadHeaderEnvelope(Container): - header: ExecutionPayloadHeader - builder_index: ValidatorIndex - value: Gwei -``` - -#### `SignedExecutionPayloadHeaderEnvelope` +#### `SignedExecutionPayloadHeader` ```python -class SignedExecutionPayloadHeaderEnvelope(Container): - message: ExecutionPayloadHeaderEnvelope +class SignedExecutionPayloadHeader(Container): + message: ExecutionPayloadHeader signature: BLSSignature ``` @@ -297,6 +220,7 @@ class ExecutionPayloadEnvelope(Container): blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] inclusion_list_proposer_index: ValidatorIndex inclusion_list_signature: BLSSignature + payload_withheld: bool state_root: Root ``` @@ -308,20 +232,12 @@ class SignedExecutionPayloadEnvelope(Container): signature: BLSSignature ``` -#### `InclusionListSummaryEntry` - -```python -class InclusionListSummaryEntry(Container): - address: ExecutionAddress - gas_limit: uint64 -``` - #### `InclusionListSummary` ```python class InclusionListSummary(Container) proposer_index: ValidatorIndex - summary: List[InclusionListSummaryEntry, MAX_TRANSACTIONS_PER_INCLUSION_LIST] + summary: List[ExecutionAddress, MAX_TRANSACTIONS_PER_INCLUSION_LIST] ``` #### `SignedInclusionListSummary` @@ -343,22 +259,6 @@ class InclusionList(Container) ### Modified containers -#### `Validator` -**Note:** The `Validator` class is modified to keep track of the slashed categories. - -```python -class Validator(Container): - pubkey: BLSPubkey - withdrawal_credentials: Bytes32 # Commitment to pubkey for withdrawals - effective_balance: Gwei # Balance at stake - slashed: uint8 # (Modified in ePBS) - # Status epochs - activation_eligibility_epoch: Epoch # When criteria for activation were met - activation_epoch: Epoch - exit_epoch: Epoch - withdrawable_epoch: Epoch # When validator can withdraw funds -``` - #### `BeaconBlockBody` **Note:** The Beacon Block body is modified to contain a Signed `ExecutionPayloadHeader`. The containers `BeaconBlock` and `SignedBeaconBlock` are modified indirectly. @@ -380,15 +280,13 @@ class BeaconBlockBody(Container): # Removed blob_kzg_commitments [Removed in ePBS] bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] # PBS - signed_execution_payload_header_envelope: SignedExecutionPayloadHeaderEnvelope # [New in ePBS] + signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] payload_attestations: List[PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS] # [New in ePBS] - execution_payload_withdraw_requests: List[ExecutionLayerWithdrawRequest, MAX_EXECUTION_LAYER_WITHDRAW_REQUESTS] # [New in ePBS] - ``` #### `ExecutionPayload` -**Note:** The `ExecutionPayload` is modified to contain a transaction inclusion list summary signed by the corresponding beacon block proposer and the list of indices of transactions in the parent block that have to be excluded from the inclusion list summary because they were satisfied in the previous slot. +**Note:** The `ExecutionPayload` is modified to contain a transaction inclusion list summary signed by the corresponding beacon block proposer ```python class ExecutionPayload(Container): @@ -411,41 +309,22 @@ class ExecutionPayload(Container): withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] blob_gas_used: uint64 excess_blob_gas: uint64 - inclusion_list_summary: List[InclusionListSummaryEntry, MAX_TRANSACTIONS_PER_INCLUSION_LIST] # [New in ePBS] - inclusion_list_exclusions: List[uint64, MAX_TRANSACTIONS_PER_INCLUSION_LIST] # [New in ePBS] + inclusion_list_summary: InclusionListSummary # [New in ePBS] ``` #### `ExecutionPayloadHeader` -**Note:** The `ExecutionPayloadHeader` is modified to account for the transactions inclusion lists. +**Note:** The `ExecutionPayloadHeader` is modified to only contain the block hash of the committed `ExecutionPayload` in addition to the builder's payment information. ```python class ExecutionPayloadHeader(Container): - # Execution block header fields - parent_hash: Hash32 - fee_recipient: ExecutionAddress - state_root: Bytes32 - receipts_root: Bytes32 - logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] - prev_randao: Bytes32 - block_number: uint64 - gas_limit: uint64 - gas_used: uint64 - timestamp: uint64 - extra_data: ByteList[MAX_EXTRA_DATA_BYTES] - base_fee_per_gas: uint256 - # Extra payload fields - block_hash: Hash32 # Hash of execution block - transactions_root: Root - withdrawals_root: Root - blob_gas_used: uint64 - excess_blob_gas: uint64 - inclusion_list_summary_root: Root # [New in ePBS] - inclusion_list_exclusions_root: Root # [New in ePBS] + block_hash: Hash32 + builder_index: ValidatorIndex + value: Gwei ``` #### `BeaconState` -*Note*: the beacon state is modified to store a signed latest execution payload header, to track the last withdrawals and increased Maximum effective balance fields: `deposit_balance_to_consume`, `exit_balance_to_consume` and `earliest_exit_epoch`. +*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. ```python class BeaconState(Container): @@ -484,7 +363,7 @@ class BeaconState(Container): current_sync_committee: SyncCommittee next_sync_committee: SyncCommittee # Execution - latest_execution_payload_header: ExecutionPayloadHeader + # latest_execution_payload_header: ExecutionPayloadHeader # [Removed in ePBS] # Withdrawals next_withdrawal_index: WithdrawalIndex next_withdrawal_validator_index: ValidatorIndex @@ -493,13 +372,10 @@ class BeaconState(Container): # PBS previous_inclusion_list_proposer: ValidatorIndex # [New in ePBS] latest_inclusion_list_proposer: ValidatorIndex # [New in ePBS] - signed_execution_payload_header_envelope: SignedExecutionPayloadHeaderEnvelope # [New in ePBS] + latest_block_hash: Hash32 # [New in ePBS] + latest_full_slot: Slot # [New in ePBS] + signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] last_withdrawals_root: Root # [New in ePBS] - deposit_balance_to_consume: Gwei # [New in ePBS] - exit_balance_to_consume: Gwei # [New in ePBS] - earliest_exit_epoch: Epoch # [New in ePBS] - pending_balance_deposits: List[PendingBalanceDeposit, MAX_PENDING_BALANCE_DEPOSITS] # [New in ePBS] - pending_partial_withdrawals: List[PartialWithdrawals, MAX_PENDING_PARTIAL_WITHDRAWALS] # [New in ePBS] ``` ## Helper functions @@ -520,87 +396,6 @@ def bit_floor(n: uint64) -> uint64: ### Predicates -#### `is_builder` - -```python -def is_builder(validator: Validator) -> bool: - """ - Check if `validator` is a registered builder - """ - return validator.withdrawal_credentials[0] == BUILDER_WITHDRAWAL_PREFIX -``` - -#### `is_eligible_for_activation_queue` - -```python -def is_eligible_for_activation_queue(validator: Validator) -> bool: - """ - Check if ``validator`` is eligible to be placed into the activation queue. - """ - return ( - validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH - and validator.effective_balance >= MIN_ACTIVATION_BALANCE - ) -``` - -#### `is_slashed_proposer` - -```python -def is_slashed_proposer(validator: Validator) -> bool: - """ - return ``true`` if ``validator`` has committed a proposer equivocation - """ - return has_flag(ParticipationFlags(validator.slashed), SLASHED_PROPOSER_FLAG_INDEX) -``` - -#### `is_slashed_attester` - -```python -def is_slashed_attester(validator: Validator) -> bool: - """ - return ``true`` if ``validator`` has committed an attestation slashing offense - """ - return has_flag(ParticipationFlags(validator.slashed), SLASHED_ATTESTSER_FLAG_INDEX) -``` - - -#### Modified `is_slashable_validator` -**Note:** The function `is_slashable_validator` is modified and renamed to `is_attester_slashable_validator`. - -```python -def is_attester_slashable_validator(validator: Validator, epoch: Epoch) -> bool: - """ - Check if ``validator`` is slashable. - """ - return (not is_slashed_attester(validator)) and (validator.activation_epoch <= epoch < validator.withdrawable_epoch) -``` - -#### Modified `is_fully_withdrawable_validator` - -```python -def is_fully_withdrawable_validator(validator: Validator, balance: Gwei, epoch: Epoch) -> bool: - """ - Check if ``validator`` is fully withdrawable. - """ - return ( - (has_eth1_withdrawal_credential(validator) or is_builder(validator)) - and validator.withdrawable_epoch <= epoch - and balance > 0 - ) -``` - -#### `is_partially_withdrawable_validator` - -```python -def is_partially_withdrawable_validator(validator: Validator, balance: Gwei) -> bool: - """ - Check if ``validator`` is partially withdrawable. - """ - if not (has_eth1_withdrawal_credential(validator) or is_builder(validator)): - return False - return get_validator_excess_balance(validator, balance) > 0 -``` - #### `is_valid_indexed_payload_attestation` ```python @@ -608,6 +403,10 @@ def is_valid_indexed_payload_attestation(state: BeaconState, indexed_payload_att """ 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)): @@ -621,25 +420,15 @@ def is_valid_indexed_payload_attestation(state: BeaconState, indexed_payload_att #### `is_parent_block_full` +This function returns true if the last committed payload header was fulfilled with a full payload, this can only happen when the parent block was full, that is both beacon block and payload were present. + ```python def is_parent_block_full(state: BeaconState) -> bool: - return state.signed_execution_payload_header_envelope.message.header == state.latest_execution_payload_header + return state.signed_execution_payload_header.message.block_hash == state.latest_block_hash ``` ### Beacon State accessors -#### Modified `get_eligible_validator_indices` -**Note:** The function `get_eligible_validator_indices` is modified to use the new flag mechanism for slashings. - -```python -def get_eligible_validator_indices(state: BeaconState) -> Sequence[ValidatorIndex]: - previous_epoch = get_previous_epoch(state) - return [ - ValidatorIndex(index) for index, v in enumerate(state.validators) - if is_active_validator(v, previous_epoch) or (is_slashed_attester(v) and previous_epoch + 1 < v.withdrawable_epoch) - ] -``` - #### `get_ptc` ```python @@ -654,16 +443,15 @@ def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]: validator_indices = [] for idx in range(committees_per_slot) beacon_committee = get_beacon_committee(state, slot, idx) - vals = [idx for idx in beacon_committee if not is_builder(idx)] - validator_indices += vals[:members_per_commitee] + validator_indices += beacon_committee[:members_per_commitee] return validator_indices ``` #### `get_payload_attesting_indices` ```python -def get_payload_attesting_indices(state: BeaconState, - slot: Slot, payload_attestation: PayloadAttestation) -> Set[ValidatorIndex]: +def get_payload_attesting_indices(state: BeaconState, slot: Slot, + payload_attestation: PayloadAttestation) -> Set[ValidatorIndex]: """ Return the set of attesting indices corresponding to ``payload_attestation``. """ @@ -675,8 +463,8 @@ def get_payload_attesting_indices(state: BeaconState, #### `get_indexed_payload_attestation` ```python -def get_indexed_payload_attestation(state: BeaconState, - slot: Slot, payload_attestation: PayloadAttestation) -> IndexedPayloadAttestation: +def get_indexed_payload_attestation(state: BeaconState, slot: Slot, + payload_attestation: PayloadAttestation) -> IndexedPayloadAttestation: """ Return the indexed payload attestation corresponding to ``payload_attestation``. """ @@ -689,391 +477,96 @@ def get_indexed_payload_attestation(state: BeaconState, ) ``` -#### `get_validator_excess_balance` - -```python -def get_validator_excess_balance(validator: Validator, balance: Gwei) -> Gwei: - if has_eth1_withdrawal_credential(validator) and balance > MIN_ACTIVATION_BALANCE: - return balance - MIN_ACTIVATION_BALANCE - if is_builder(validator) and balance > MAX_EFFECTIVE_BALANCE: - return balance - MAX_EFFECTIVE_BALANCE - return Gwei(0) -``` - -#### Modified `get_validator_churn_limit` - -```python -def get_validator_churn_limit(state: BeaconState) -> Gwei: - """ - Return the validator churn limit for the current epoch. - """ - churn = max(MIN_PER_EPOCH_CHURN_LIMIT * MIN_ACTIVATION_BALANCE, get_total_active_balance(state) // CHURN_LIMIT_QUOTIENT) - return churn - churn % EFFECTIVE_BALANCE_INCREMENT -``` - -#### Modified `get_expected_withdrawals` -**Note:** the function `get_expected_withdrawals` is modified to churn the withdrawals by balance because of the increase in `MAX_EFFECTIVE_BALANCE` - -```python -def get_expected_withdrawals(state: BeaconState) -> Sequence[Withdrawal]: - epoch = get_current_epoch(state) - withdrawal_index = state.next_withdrawal_index - validator_index = state.next_withdrawal_validator_index - withdrawals: List[Withdrawal] = [] - consumed = 0 - for withdrawal in state.pending_partial_withdrawals: - if withdrawal.withdrawable_epoch > epoch or len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD // 2: - break - validator = state.validators[withdrawal.index] - if validator.exit_epoch == FAR_FUTURE_EPOCH and state.balances[withdrawal.index] > MIN_ACTIVATION_BALANCEa: - withdrawble_balance = min(state.balances[withdrawal.index] - MIN_ACTIVATION_BALANCE, withdrawal.amount) - withdrawals.append(Withdrawal( - index=withdrawal_index, - validator_index=withdrawal.index, - address=ExecutionAddress(validator.withdrawal_credentials[12:]), - amount=withdrawable_balance, - )) - withdrawal_index += WithdrawalIndex(1) - consumed += 1 - state.pending_partial_withdrawals = state.pending_partial_withdrawals[consumed:] - - # Sweep for remaining - bound = min(len(state.validators), MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP) - for _ in range(bound): - validator = state.validators[validator_index] - balance = state.balances[validator_index] - if is_fully_withdrawable_validator(validator, balance, epoch): - withdrawals.append(Withdrawal( - index=withdrawal_index, - validator_index=validator_index, - address=ExecutionAddress(validator.withdrawal_credentials[12:]), - amount=balance, - )) - withdrawal_index += WithdrawalIndex(1) - elif is_partially_withdrawable_validator(validator, balance): - withdrawals.append(Withdrawal( - index=withdrawal_index, - validator_index=validator_index, - address=ExecutionAddress(validator.withdrawal_credentials[12:]), - amount=get_validator_excess_balance(validator, balance), - )) - withdrawal_index += WithdrawalIndex(1) - if len(withdrawals) == MAX_WITHDRAWALS_PER_PAYLOAD: - break - validator_index = ValidatorIndex((validator_index + 1) % len(state.validators)) - return withdrawals -``` - -### Beacon state mutators - -#### `compute_exit_epoch_and_update_churn` - -```python -def compute_exit_epoch_and_update_churn(state: BeaconState, exit_balance: Gwei) -> Epoch: - earliest_exit_epoch = compute_activation_exit_epoch(get_current_epoch(state)) - per_epoch_churn = get_validator_churn_limit(state) - # New epoch for exits. - if state.earliest_exit_epoch < earliest_exit_epoch: - state.earliest_exit_epoch = earliest_exit_epoch - state.exit_balance_to_consume = per_epoch_churn - - # Exit fits in the current earliest epoch. - if exit_balance < state.exit_balance_to_consume: - state.exit_balance_to_consume -= exit_balance - else: # Exit doesn't fit in the current earliest epoch. - balance_to_process = exit_balance - state.exit_balance_to_consume - additional_epochs, remainder = divmod(balance_to_process, per_epoch_churn) - state.earliest_exit_epoch += additional_epochs - state.exit_balance_to_consume = per_epoch_churn - remainder - return state.earliest_exit_epoch -``` - -#### Modified `initiate_validator_exit` -**Note:** the function `initiate_validator_exit` is modified to use the new churn mechanism. - -```python -def initiate_validator_exit(state: BeaconState, index: ValidatorIndex) -> None: - """ - Initiate the exit of the validator with index ``index``. - """ - # Return if validator already initiated exit - validator = state.validators[index] - if validator.exit_epoch != FAR_FUTURE_EPOCH: - return - - # Compute exit queue epoch - exit_queue_epoch = compute_exit_epoch_and_update_churn(state, state.balances[index]) - - # Set validator exit epoch and withdrawable epoch - validator.exit_epoch = exit_queue_epoch - validator.withdrawable_epoch = Epoch(validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY) -``` - -#### Modified `slash_validator` -**Note:** The function `slash_validator` is modified to use the new flag system. - -```python -def slash_validator(state: BeaconState, - slashed_index: ValidatorIndex, - whistleblower_index: ValidatorIndex=None) -> None: - """ - Slash the validator with index ``slashed_index``. - """ - epoch = get_current_epoch(state) - initiate_validator_exit(state, slashed_index) - validator = state.validators[slashed_index] - validator.slashed = add_flag(validator.slashed, SLASHED_ATTESTER_FLAG_INDEX) - validator.withdrawable_epoch = max(validator.withdrawable_epoch, Epoch(epoch + EPOCHS_PER_SLASHINGS_VECTOR)) - state.slashings[epoch % EPOCHS_PER_SLASHINGS_VECTOR] += validator.effective_balance - decrease_balance(state, slashed_index, validator.effective_balance // MIN_SLASHING_PENALTY_QUOTIENT) - - # Apply proposer and whistleblower rewards - proposer_index = get_beacon_proposer_index(state) - if whistleblower_index is None: - whistleblower_index = proposer_index - whistleblower_reward = Gwei(validator.effective_balance // WHISTLEBLOWER_REWARD_QUOTIENT) - proposer_reward = Gwei(whistleblower_reward // PROPOSER_REWARD_QUOTIENT) - increase_balance(state, proposer_index, proposer_reward) - increase_balance(state, whistleblower_index, Gwei(whistleblower_reward - proposer_reward)) -``` - -## Genesis - -### Modified `initialize_beacon_statre_from_eth1` - -```python -def initialize_beacon_state_from_eth1(eth1_block_hash: Hash32, - eth1_timestamp: uint64, - deposits: Sequence[Deposit]) -> BeaconState: - fork = Fork( - previous_version=GENESIS_FORK_VERSION, - current_version=GENESIS_FORK_VERSION, - epoch=GENESIS_EPOCH, - ) - state = BeaconState( - genesis_time=eth1_timestamp + GENESIS_DELAY, - fork=fork, - eth1_data=Eth1Data(block_hash=eth1_block_hash, deposit_count=uint64(len(deposits))), - latest_block_header=BeaconBlockHeader(body_root=hash_tree_root(BeaconBlockBody())), - randao_mixes=[eth1_block_hash] * EPOCHS_PER_HISTORICAL_VECTOR, # Seed RANDAO with Eth1 entropy - ) - - # Process deposits - leaves = list(map(lambda deposit: deposit.data, deposits)) - for index, deposit in enumerate(deposits): - deposit_data_list = List[DepositData, 2**DEPOSIT_CONTRACT_TREE_DEPTH](*leaves[:index + 1]) - state.eth1_data.deposit_root = hash_tree_root(deposit_data_list) - process_deposit(state, deposit) - - # Process activations - for index, validator in enumerate(state.validators): - balance = state.balances[index] - validator.effective_balance = min(balance - balance % EFFECTIVE_BALANCE_INCREMENT, MAX_EFFECTIVE_BALANCE) - if validator.effective_balance >= MIN_ACTIVATION_BALANCE: - validator.activation_eligibility_epoch = GENESIS_EPOCH - validator.activation_epoch = GENESIS_EPOCH - - # Set genesis validators root for domain separation and chain versioning - state.genesis_validators_root = hash_tree_root(state.validators) - - return state -``` - ## 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. +*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. -### Epoch processing - -#### Modified `process_epoch` - -```python -def process_epoch(state: BeaconState) -> None: - process_justification_and_finalization(state) - process_inactivity_updates(state) - process_rewards_and_penalties(state) - process_registry_updates(state) # [Modified in ePBS] - process_slashings(state) - process_eth1_data_reset(state) - process_pending_balance_deposits(state) # [New in ePBS] - process_effective_balance_updates(state) # [Modified in ePBS] - process_slashings_reset(state) # [Modified in ePBS] - process_randao_mixes_reset(state) - process_historical_summaries_update(state) - process_participation_flag_updates(state) - process_sync_committee_updates(state) - process_builder_updates(state) # [New in ePBS] -``` - -#### Helper functions - -##### Modified `process_registry_updates` +### Block processing -```python -def process_registry_updates(state: BeaconState) -> None: - # Process activation eligibility and ejections - for index, validator in enumerate(state.validators): - if is_eligible_for_activation_queue(validator): - validator.activation_eligibility_epoch = get_current_epoch(state) + 1 - - if ( - is_active_validator(validator, get_current_epoch(state)) - and validator.effective_balance <= EJECTION_BALANCE - ): - initiate_validator_exit(state, ValidatorIndex(index)) - - # Activate all eligible validators - activation_epoch = compute_activation_exit_epoch(get_current_epoch(state)) - for validator in state.validators: - if is_eligible_for_activation(state, validator): - validator.activation_epoch = activation_epoch -``` +*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. -##### `process_pending_balance_deposits` ```python -def process_pending_balance_deposits(state: BeaconState) -> None: - state.deposit_balance_to_consume += get_validator_churn_limit(state) - next_pending_deposit_index = 0 - for pending_balance_deposit in state.pending_balance_deposits: - if state.deposit_balance_to_consume < pending_balance_deposit.amount: - break - - state.deposit_balance_to_consume -= pending_balance_deposit.amount - increase_balance(state, pending_balance_deposit.index, pending_balance_deposit.amount) - next_pending_deposit_index += 1 - - state.pending_balance_deposits = state.pending_balance_deposits[next_pending_deposit_index:] +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_effective_balance_updates` +#### Modified `process_withdrawals` +**Note:** This is modified to take only the `state` parameter. The payload is required to honor these withdrawals. ```python -def process_effective_balance_updates(state: BeaconState) -> None: - # Update effective balances with hysteresis - for index, validator in enumerate(state.validators): - balance = state.balances[index] - HYSTERESIS_INCREMENT = uint64(EFFECTIVE_BALANCE_INCREMENT // HYSTERESIS_QUOTIENT) - DOWNWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_DOWNWARD_MULTIPLIER - UPWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_UPWARD_MULTIPLIER - EFFECTIVE_BALANCE_LIMIT = MAX_EFFECTIVE_BALANCE if is_builder(validator) else MIN_ACTIVATION_BALANCE - if ( - balance + DOWNWARD_THRESHOLD < validator.effective_balance - or validator.effective_balance + UPWARD_THRESHOLD < balance - ): - validator.effective_balance = min(balance - balance % EFFECTIVE_BALANCE_INCREMENT, EFFECTIVE_BALANCE_LIMIT) -``` - -##### Modified `process_slashings` -**Note:** The only modification is to use the new flag mechanism +def process_withdrawals(state: BeaconState) -> None: + ## return early if the parent block was empty + if !is_parent_block_full(state): + return -```python -def process_slashings(state: BeaconState) -> None: - epoch = get_current_epoch(state) - total_balance = get_total_active_balance(state) - adjusted_total_slashing_balance = min(sum(state.slashings) * PROPORTIONAL_SLASHING_MULTIPLIER, total_balance) - for index, validator in enumerate(state.validators): - if is_slashed_attester(validator) and epoch + EPOCHS_PER_SLASHINGS_VECTOR // 2 == validator.withdrawable_epoch: - increment = EFFECTIVE_BALANCE_INCREMENT # Factored out from penalty numerator to avoid uint64 overflow - penalty_numerator = validator.effective_balance // increment * adjusted_total_slashing_balance - penalty = penalty_numerator // total_balance * increment - decrease_balance(state, ValidatorIndex(index), penalty) -``` + 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) -##### Modified `get_unslashed_attesting_indices` -**Note:** The function `get_unslashed_attesting_indices` is modified to return only the attester slashing validators. + # 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) -```python -def get_unslashed_participating_indices(state: BeaconState, flag_index: int, epoch: Epoch) -> Set[ValidatorIndex]: - """ - Return the set of validator indices that are both active and unslashed for the given ``flag_index`` and ``epoch``. - """ - assert epoch in (get_previous_epoch(state), get_current_epoch(state)) - if epoch == get_current_epoch(state): - epoch_participation = state.current_epoch_participation + # 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: - epoch_participation = state.previous_epoch_participation - active_validator_indices = get_active_validator_indices(state, epoch) - participating_indices = [i for i in active_validator_indices if has_flag(epoch_participation[i], flag_index)] - return set(filter(lambda index: not is_slashed_attester(state.validators[index]), participating_indices)) + # 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 ``` -### Execution engine - -#### Request data - -##### New `NewInclusionListRequest` +#### New `verify_execution_payload_header_signature` ```python -@dataclass -class NewInclusionListRequest(object): - inclusion_list: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] - summary: List[InclusionListSummaryEntry, MAX_TRANSACTIONS_PER_INCLUSION_LIST] - parent_block_hash: Hash32 +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) ``` - -#### Engine APIs -#### New `notify_new_inclusion_list` +#### New `process_execution_payload_header` ```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 - """ - ... -``` - -### 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. +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 + # 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 -```python -def process_block(state: BeaconState, block: BeaconBlock) -> None: - process_block_header(state, block) # [Modified in ePBS] - 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) + # Cache the signed execution payload header + state.signed_execution_payload_header = signed_header ``` -#### Modified `process_block_header` -**Note:** the only modification is in the `slashed` verification. - -```python -def process_block_header(state: BeaconState, block: BeaconBlock) -> None: - # Verify that the slots match - assert block.slot == state.slot - # Verify that the block is newer than latest block header - assert block.slot > state.latest_block_header.slot - # Verify that proposer index is the correct index - assert block.proposer_index == get_beacon_proposer_index(state) - # Verify that the parent matches - assert block.parent_root == hash_tree_root(state.latest_block_header) - # Cache current block as the new latest block - state.latest_block_header = BeaconBlockHeader( - slot=block.slot, - proposer_index=block.proposer_index, - parent_root=block.parent_root, - state_root=Bytes32(), # Overwritten in the next process_slot call - body_root=hash_tree_root(block.body), - ) - - # Verify proposer is not slashed - proposer = state.validators[block.proposer_index] - assert proposer.slashed == uint8(0) -``` #### Modified `process_operations` @@ -1088,70 +581,15 @@ def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: for operation in operations: fn(state, operation) - for_ops(body.proposer_slashings, process_proposer_slashing) # [Modified in ePBS] - for_ops(body.attester_slashings, process_attester_slashing) # [Modified in ePBS] - for_ops(body.attestations, process_attestation) + 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] - for_ops(body.execution_payload_withdraw_requests, process_execution_layer_withdraw_request) # [New in ePBS] -``` - -##### Modified Proposer slashings - -```python -def process_proposer_slashing(state: BeaconState, proposer_slashing: ProposerSlashing) -> None: - header_1 = proposer_slashing.signed_header_1.message - header_2 = proposer_slashing.signed_header_2.message - - # Verify header slots match - assert header_1.slot == header_2.slot - # Verify header proposer indices match - assert header_1.proposer_index == header_2.proposer_index - # Verify the headers are different - assert header_1 != header_2 - # Verify the proposer is slashable - proposer = state.validators[header_1.proposer_index] - assert proposer.activation_epoch <= get_current_epoch(state) and not is_slashed_proposer(proposer) - # Verify signatures - for signed_header in (proposer_slashing.signed_header_1, proposer_slashing.signed_header_2): - domain = get_domain(state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(signed_header.message.slot)) - signing_root = compute_signing_root(signed_header.message, domain) - assert bls.Verify(proposer.pubkey, signing_root, signed_header.signature) - - # Apply penalty - penalty = PROPOSER_EQUIVOCATION_PENALTY_FACTOR * EFFECTIVE_BALANCE_INCREMENT - decrease_balance(state, header_1.proposer_index, penalty) - initiate_validator_exit(state, header_1.proposer_index) - proposer.slashed = add_flag(proposer.slashed, SLASHED_PROPOSER_FLAG_INDEX) - - # Apply proposer and whistleblower rewards - proposer_reward = Gwei((penalty // WHISTLEBLOWER_REWARD_QUOTIENT) * PROPOSER_WEIGHT // WEIGHT_DENOMINATOR) - increase_balance(state, get_beacon_proposer_index(state), proposer_reward) -``` - -##### Modified Attester slashings -**Note:** The only modification is the use of `is_attester_slashable_validator` - -```python -def process_attester_slashing(state: BeaconState, attester_slashing: AttesterSlashing) -> None: - attestation_1 = attester_slashing.attestation_1 - attestation_2 = attester_slashing.attestation_2 - assert is_slashable_attestation_data(attestation_1.data, attestation_2.data) - assert is_valid_indexed_attestation(state, attestation_1) - assert is_valid_indexed_attestation(state, attestation_2) - - slashed_any = False - indices = set(attestation_1.attesting_indices).intersection(attestation_2.attesting_indices) - for index in sorted(indices): - if is_attester_slashable_validator(state.validators[index], get_current_epoch(state)): - slash_validator(state, index) - slashed_any = True - assert slashed_any ``` - ##### Modified `process_attestation` *Note*: The function `process_attestation` is modified to ignore attestations from the ptc @@ -1194,52 +632,6 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None: increase_balance(state, get_beacon_proposer_index(state), proposer_reward) ``` -##### Modified `get_validator_from_deposit` -**Note:** The function `get_validator_from_deposit` is modified to take only a pubkey and withdrawal credentials and sets the effective balance to zero - -```python -def get_validator_from_deposit(pubkey: BLSPubkey, withdrawal_credentials: Bytes32) -> Validator: - return Validator( - pubkey=pubkey, - withdrawal_credentials=withdrawal_credentials, - activation_eligibility_epoch=FAR_FUTURE_EPOCH, - activation_epoch=FAR_FUTURE_EPOCH, - exit_epoch=FAR_FUTURE_EPOCH, - withdrawable_epoch=FAR_FUTURE_EPOCH, - effective_balance=0, - ) -``` - -##### Modified `apply_deposit` - -```python -def apply_deposit(state: BeaconState, - pubkey: BLSPubkey, - withdrawal_credentials: Bytes32, - amount: uint64, - signature: BLSSignature) -> None: - validator_pubkeys = [v.pubkey for v in state.validators] - if pubkey not in validator_pubkeys: - # Verify the deposit signature (proof of possession) which is not checked by the deposit contract - deposit_message = DepositMessage( - pubkey=pubkey, - withdrawal_credentials=withdrawal_credentials, - amount=amount, - ) - domain = compute_domain(DOMAIN_DEPOSIT) # Fork-agnostic domain since deposits are valid across forks - signing_root = compute_signing_root(deposit_message, domain) - if bls.Verify(pubkey, signing_root, signature): - index = len(state.validators) - state.validators.append(get_validator_from_deposit(pubkey, withdrawal_credentials)) - state.balances.append(0) - state.previous_epoch_participation.append(ParticipationFlags(0b0000_0000)) - state.current_epoch_participation.append(ParticipationFlags(0b0000_0000)) - state.inactivity_scores.append(uint64(0)) - else: - index = ValidatorIndex(validator_pubkeys.index(pubkey)) - state.pending_balance_deposits.append(PendingBalanceDeposit(index=index, amount=amount)) -``` - ##### Payload Attestations ```python @@ -1261,10 +653,9 @@ def process_payload_attestation(state: BeaconState, payload_attestation: Payload epoch_participation = state.current_epoch_participation # Return early if the attestation is for the wrong payload status - latest_payload_timestamp = state.latest_execution_payload_header.timestamp - present_timestamp = compute_timestamp_at_slot(state, data.slot) - payload_was_present = latest_payload_timestamp == present_timestamp - if data.payload_present != payload_was_present: + payload_was_present = data.slot == state.latest_full_slot + voted_preset = data.payload_status == PAYLOAD_PRESENT + if voted_present != payload_was_present: return # Reward the proposer and set all the participation flags proposer_reward_numerator = 0 @@ -1280,115 +671,10 @@ def process_payload_attestation(state: BeaconState, payload_attestation: Payload increase_balance(state, get_beacon_proposer_index(state), proposer_reward) ``` -##### Execution Layer Withdraw Requests - -```python -def process_execution_layer_withdraw_request( - state: BeaconState, - execution_layer_withdraw_request: ExecutionLayerWithdrawRequest - ) -> None: - validator_pubkeys = [v.pubkey for v in state.validators] - validator_index = ValidatorIndex(validator_pubkeys.index(execution_layer_withdraw_request.validator_pubkey)) - validator = state.validators[validator_index] - - # Same conditions as in EIP7002 https://github.com/ethereum/consensus-specs/pull/3349/files#diff-7a6e2ba480d22d8bd035bd88ca91358456caf9d7c2d48a74e1e900fe63d5c4f8R223 - # Verify withdrawal credentials - assert validator.withdrawal_credentials[:1] == ETH1_ADDRESS_WITHDRAWAL_PREFIX - assert validator.withdrawal_credentials[12:] == execution_layer_withdraw_request.source_address - assert is_active_validator(validator, get_current_epoch(state)) - # Verify exit has not been initiated, and slashed - assert validator.exit_epoch == FAR_FUTURE_EPOCH: - # Verify the validator has been active long enough - assert get_current_epoch(state) >= validator.activation_epoch + config.SHARD_COMMITTEE_PERIOD - - pending_balance_to_withdraw = sum(item.amount for item in state.pending_partial_withdrawals if item.index == validator_index) - - available_balance = state.balances[validator_index] - MIN_ACTIVATION_BALANCE - pending_balance_to_withdraw - assert available_balance >= execution_layer_withdraw_request.balance - - exit_queue_epoch = compute_exit_epoch_and_update_churn(state, available_balance) - withdrawable_epoch = Epoch(exit_queue_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY) - - state.pending_partial_withdrawals.append(PartialWithdrawal( - index=validator_index, - amount=available_balance, - withdrawable_epoch=withdrawable_epoch, - )) -``` - -#### Modified `process_withdrawals` -**Note:** TODO: This is modified to take only the State as parameter as they are deterministic. +#### New `verify_execution_payload_envelope_signature` ```python -def process_withdrawals(state: BeaconState) -> None: - ## return early if the parent block was empty - state.signed_execution_payload_header_envelope.message.header != state.latest_execution_payload_header: - 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_envelope_signature` - -```python -def verify_execution_payload_header_envelope_signature(state: BeaconState, - signed_header_envelope: SignedExecutionPayloadHeaderEnvelope) -> bool: - # Check the signature - builder = state.validators[signed_header_envelope.message.builder_index] - signing_root = compute_signing_root(signed_header_envelope.message, get_domain(state, DOMAIN_BEACON_BUILDER)) - return bls.Verify(builder.pubkey, signing_root, signed_header_envelope.signature) -``` - -#### New `process_execution_payload_header` - -```python -def process_execution_payload_header(state: BeaconState, block: BeaconBlock) -> None: - signed_header_envelope = block.body.signed_execution_payload_header_envelope - assert verify_execution_payload_header_envelope_signature(state, signed_header_envelope) - # Check that the builder has funds to cover the bid and schedule the funds for transfer - envelope = signed_header_envelope.message - builder_index = envelope.builder_index - amount = envelope.value - assert state.balances[builder_index] >= amount: - decrease_balance(state, builder_index, amount) - state.pending_balance_deposits.append(PendingBalanceDeposit(index=block.proposer_index, amount=amount)) - # Verify the withdrawals_root against the state cached ones - assert header.withdrawals_root == state.last_withdrawals_root - # Verify consistency of the parent hash with respect to the previous execution payload header - assert header.parent_hash == state.latest_execution_payload_header.block_hash - # Verify prev_randao - assert header.prev_randao == get_randao_mix(state, get_current_epoch(state)) - # Verify timestamp - assert header.timestamp == compute_timestamp_at_slot(state, state.slot) - # 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 - # Cache execution payload header envelope - state.signed_execution_payload_header_envelope = signed_header_envelope -``` - -#### New `verify_execution_payload_signature` - -```python -def verify_execution_envelope_signature(state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope) -> bool: +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) @@ -1398,7 +684,6 @@ def verify_execution_envelope_signature(state: BeaconState, signed_envelope: Sig ```python def verify_inclusion_list_summary_signature(state: BeaconState, signed_summary: SignedInclusionListSummary) -> bool: - # TODO: do we need a new domain? summary = signed_summary.message signing_root = compute_signing_root(summary, get_domain(state, DOMAIN_BEACON_PROPOSER)) proposer = state.validators[message.proposer_index] @@ -1410,7 +695,7 @@ def verify_inclusion_list_summary_signature(state: BeaconState, signed_summary: ```python def process_execution_payload(state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope, execution_engine: ExecutionEngine) -> None: - # Verify signature [New in ePBS] + # Verify signature assert verify_execution_envelope_signature(state, signed_envelope) envelope = signed_envelope.message payload = envelope.payload @@ -1427,12 +712,16 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti # Verify consistency with the beacon block assert envelope.beacon_block_root == hash_tree_root(state.latest_block_header) # Verify consistency with the committed header - hash = hash_tree_root(payload) - committed_envelope = state.signed_execution_payload_header_envelope.message - previous_hash = hash_tree_root(committed_envelope.payload) - assert hash == previous_hash + committed_header = state.signed_execution_payload_header.message + assert committed_header.block_hash == payload.block_hash # Verify consistency with the envelope - assert envelope.builder_index == committed_envelope.builder_index + 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( @@ -1443,7 +732,8 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti ) ) # Cache the execution payload header and proposer - state.latest_execution_payload_header = committed_envelope.header + state.latest_block_hash = payload.block_hash + state.latest_full_slot = state.slot state.previous_inclusion_list_proposer = state.latest_inclusion_list_proposer # Verify the state root assert envelope.state_root == hash_tree_root(state) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 519613f5a7..11abf21af3 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -305,7 +305,34 @@ def get_blockhash(store: Store, root: Root) -> Hash32: return hash(store.execution_payload_states[root].latest_block_header) return hash(store.block__states[root].latest_block_header) ``` - + +## 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` From 37e892be06852500a904cb27af8110084b623797 Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 2 Apr 2024 15:51:11 -0300 Subject: [PATCH 059/112] doctoc --- specs/_features/epbs/beacon-chain.md | 65 ++++------------------------ specs/_features/epbs/fork-choice.md | 3 ++ 2 files changed, 11 insertions(+), 57 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 2214e9ce7d..80d63222a4 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -8,39 +8,26 @@ - [Introduction](#introduction) - [Constants](#constants) - - [Withdrawal prefixes](#withdrawal-prefixes) - - [Slashing flags](#slashing-flags) -- [Configuration](#configuration) - - [Time parameters](#time-parameters) + - [Payload status](#payload-status) - [Preset](#preset) - [Misc](#misc) - [Domain types](#domain-types) - - [Gwei values](#gwei-values) - - [Time parameters](#time-parameters-1) - - [State list lenghts](#state-list-lenghts) - - [Rewards and penalties](#rewards-and-penalties) + - [Time parameters](#time-parameters) - [Max operations per block](#max-operations-per-block) - - [Incentivization weights](#incentivization-weights) - [Execution](#execution) - [Containers](#containers) - [New containers](#new-containers) - - [`PendingBalanceDeposit`](#pendingbalancedeposit) - - [`PartialWithdrawal`](#partialwithdrawal) - - [`ExecutionLayerWithdrawRequest`](#executionlayerwithdrawrequest) - [`PayloadAttestationData`](#payloadattestationdata) - [`PayloadAttestation`](#payloadattestation) - [`PayloadAttestationMessage`](#payloadattestationmessage) - [`IndexedPayloadAttestation`](#indexedpayloadattestation) - - [`ExecutionPayloadHeaderEnvelope`](#executionpayloadheaderenvelope) - - [`SignedExecutionPayloadHeaderEnvelope`](#signedexecutionpayloadheaderenvelope) + - [`SignedExecutionPayloadHeader`](#signedexecutionpayloadheader) - [`ExecutionPayloadEnvelope`](#executionpayloadenvelope) - [`SignedExecutionPayloadEnvelope`](#signedexecutionpayloadenvelope) - - [`InclusionListSummaryEntry`](#inclusionlistsummaryentry) - [`InclusionListSummary`](#inclusionlistsummary) - [`SignedInclusionListSummary`](#signedinclusionlistsummary) - [`InclusionList`](#inclusionlist) - [Modified containers](#modified-containers) - - [`Validator`](#validator) - [`BeaconBlockBody`](#beaconblockbody) - [`ExecutionPayload`](#executionpayload) - [`ExecutionPayloadHeader`](#executionpayloadheader) @@ -49,57 +36,21 @@ - [Math](#math) - [`bit_floor`](#bit_floor) - [Predicates](#predicates) - - [`is_builder`](#is_builder) - - [`is_eligible_for_activation_queue`](#is_eligible_for_activation_queue) - - [`is_slashed_proposer`](#is_slashed_proposer) - - [`is_slashed_attester`](#is_slashed_attester) - - [Modified `is_slashable_validator`](#modified-is_slashable_validator) - - [Modified `is_fully_withdrawable_validator`](#modified-is_fully_withdrawable_validator) - - [`is_partially_withdrawable_validator`](#is_partially_withdrawable_validator) - [`is_valid_indexed_payload_attestation`](#is_valid_indexed_payload_attestation) - [`is_parent_block_full`](#is_parent_block_full) - [Beacon State accessors](#beacon-state-accessors) - - [Modified `get_eligible_validator_indices`](#modified-get_eligible_validator_indices) - [`get_ptc`](#get_ptc) - [`get_payload_attesting_indices`](#get_payload_attesting_indices) - [`get_indexed_payload_attestation`](#get_indexed_payload_attestation) - - [`get_validator_excess_balance`](#get_validator_excess_balance) - - [Modified `get_validator_churn_limit`](#modified-get_validator_churn_limit) - - [Modified `get_expected_withdrawals`](#modified-get_expected_withdrawals) - - [Beacon state mutators](#beacon-state-mutators) - - [`compute_exit_epoch_and_update_churn`](#compute_exit_epoch_and_update_churn) - - [Modified `initiate_validator_exit`](#modified-initiate_validator_exit) - - [Modified `slash_validator`](#modified-slash_validator) -- [Genesis](#genesis) - - [Modified `initialize_beacon_statre_from_eth1`](#modified--initialize_beacon_statre_from_eth1) - [Beacon chain state transition function](#beacon-chain-state-transition-function) - - [Epoch processing](#epoch-processing) - - [Modified `process_epoch`](#modified-process_epoch) - - [Helper functions](#helper-functions-1) - - [Modified `process_registry_updates`](#modified-process_registry_updates) - - [`process_pending_balance_deposits`](#process_pending_balance_deposits) - - [Modified `process_effective_balance_updates`](#modified-process_effective_balance_updates) - - [Modified `process_slashings`](#modified-process_slashings) - - [Modified `get_unslashed_attesting_indices`](#modified-get_unslashed_attesting_indices) - - [Execution engine](#execution-engine) - - [Request data](#request-data) - - [New `NewInclusionListRequest`](#new-newinclusionlistrequest) - - [Engine APIs](#engine-apis) - - [New `notify_new_inclusion_list`](#new-notify_new_inclusion_list) - [Block processing](#block-processing) - - [Modified `process_block_header`](#modified-process_block_header) + - [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 Proposer slashings](#modified-proposer-slashings) - - [Modified Attester slashings](#modified-attester-slashings) - [Modified `process_attestation`](#modified-process_attestation) - - [Modified `get_validator_from_deposit`](#modified-get_validator_from_deposit) - - [Modified `apply_deposit`](#modified-apply_deposit) - [Payload Attestations](#payload-attestations) - - [Execution Layer Withdraw Requests](#execution-layer-withdraw-requests) - - [Modified `process_withdrawals`](#modified-process_withdrawals) - - [New `verify_execution_payload_header_envelope_signature`](#new-verify_execution_payload_header_envelope_signature) - - [New `process_execution_payload_header`](#new-process_execution_payload_header) - - [New `verify_execution_payload_signature`](#new-verify_execution_payload_signature) + - [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) @@ -324,7 +275,7 @@ class ExecutionPayloadHeader(Container): ``` #### `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. +*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): diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 11abf21af3..ee4cacc893 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -21,6 +21,9 @@ - [Modified `get_weight`](#modified-get_weight) - [Modified `get_head`](#modified-get_head) - [New `get_block_hash`](#new-get_block_hash) +- [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) From 945d8bb62e4e01a504c96dadaf898aa9b3d5a1da Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 2 Apr 2024 17:16:50 -0300 Subject: [PATCH 060/112] init p2p --- specs/_features/epbs/beacon-chain.md | 55 ++---- specs/_features/epbs/p2p-interface.md | 259 ++++++++++++++++++++++++ specs/_features/epbs/validator.md | 272 ++++++++++++++++++++++++++ 3 files changed, 548 insertions(+), 38 deletions(-) create mode 100644 specs/_features/epbs/p2p-interface.md create mode 100644 specs/_features/epbs/validator.md diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 80d63222a4..3dcb2d9544 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -44,7 +44,6 @@ - [`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) @@ -203,7 +202,7 @@ class SignedInclusionListSummary(Container): ```python class InclusionList(Container) - summary: SignedInclusionListSummary + signed_summary: SignedInclusionListSummary slot: Slot transactions: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] ``` @@ -265,17 +264,20 @@ class ExecutionPayload(Container): #### `ExecutionPayloadHeader` -**Note:** The `ExecutionPayloadHeader` is modified to only contain the block hash of the committed `ExecutionPayload` in addition to the builder's payment information. +**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_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. +*Note*: the beacon state is modified to store a signed latest execution payload header. 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): @@ -326,7 +328,6 @@ class BeaconState(Container): latest_block_hash: Hash32 # [New in ePBS] latest_full_slot: Slot # [New in ePBS] signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] - last_withdrawals_root: Root # [New in ePBS] ``` ## Helper functions @@ -444,7 +445,7 @@ The post-state corresponding to a pre-state `state` and a signed execution paylo ```python def process_block(state: BeaconState, block: BeaconBlock) -> None: process_block_header(state, block) # - process_withdrawals(state) # [Modified in ePBS] + # removed process_withdrawals(state, block.body.execution_payload) [Removed 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) @@ -452,37 +453,6 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None: 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 !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 @@ -501,11 +471,18 @@ def process_execution_payload_header(state: BeaconState, block: BeaconBlock) -> # 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_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) @@ -650,6 +627,8 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti assert verify_execution_envelope_signature(state, signed_envelope) envelope = signed_envelope.message payload = envelope.payload + # Process withdrawals + process_withdrawals(state, payload) # Verify inclusion list proposer proposer_index = envelope.inclusion_list_proposer_index assert proposer_index == state.previous_inclusion_list_proposer @@ -665,7 +644,7 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti # Verify consistency with the committed header committed_header = state.signed_execution_payload_header.message assert committed_header.block_hash == payload.block_hash - # Verify consistency with the envelope + 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 diff --git a/specs/_features/epbs/p2p-interface.md b/specs/_features/epbs/p2p-interface.md new file mode 100644 index 0000000000..6fd830a164 --- /dev/null +++ b/specs/_features/epbs/p2p-interface.md @@ -0,0 +1,259 @@ +# 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` + +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. + +###### `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). + +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` +- _[REJECT]_ `payload.block_hash == header.block_hash` +- _[REJECT]_ `hash_tree_root(payload.blob_kzg_commitments) == header.blob_kzg_commitments_root` +- _[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 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 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. +- _[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_root` is a known block root 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 inclusion list with valid signature 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..b345b808fc --- /dev/null +++ b/specs/_features/epbs/validator.md @@ -0,0 +1,272 @@ + + +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [ePBS -- Honest Validator](#epbs----honest-validator) + - [Introduction](#introduction) + - [Prerequisites](#prerequisites) + - [Protocols](#protocols) + - [`ExecutionEngine`](#executionengine) + - [`get_execution_inclusion_list`](#get_execution_inclusion_list) + - [Beacon chain responsibilities](#beacon-chain-responsibilities) + - [Validator assignment](#validator-assignment) + - [Lookahead](#lookahead) + - [Inclusion list proposal](#inclusion-list-proposal) + - [Constructing the inclusion list](#constructing-the-inclusion-list) + - [Broadcast inclusion list](#broadcast-inclusion-list) + - [Block proposal](#block-proposal) + - [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) + - [Aggregation selection](#aggregation-selection) + - [Payload timeliness attestation](#payload-timeliness-attestation) + - [Constructing payload attestation](#constructing-payload-attestation) + - [Prepare payload attestation message](#prepare-payload-attestation-message) + - [Broadcast payload attestation](#broadcast-payload-attestation) + - [Attesting](#attesting) + - [Attestation aggregation](#attestation-aggregation) + - [Design Rationale](#design-rationale) + - [What is the honest behavior to build on top of a skip slot for inclusion list?](#what-is-the-honest-behavior-to-build-on-top-of-a-skip-slot-for-inclusion-list) + - [Why skip the attestation if you are assigned to PTC?](#why-skip-the-attestation-if-you-are-assigned-to-ptc) + + + +# ePBS -- Honest Validator + +**Notice**: This document is a work-in-progress for researchers and implementers. + +## Introduction + +This document represents the changes to be made in the code of an "honest validator" to implement ePBS. + +## Prerequisites + +This document is an extension of the Deneb -- Honest Validator guide. +All behaviors and definitions defined in this document, and documents it extends, carry over unless explicitly noted or overridden. + +All terminology, constants, functions, and protocol mechanics defined in the updated Beacon Chain doc of ePBS. are requisite for this document and used throughout. +Please see related Beacon Chain doc before continuing and use them as a reference throughout. + +## Protocols + +### `ExecutionEngine` + +*Note*: `get_execution_inclusion_list` function is added to the `ExecutionEngine` protocol for use as a validator. + +The body of this function is implementation dependent. +The Engine API may be used to implement it with an external execution engine. + +#### `get_execution_inclusion_list` + +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. + +```python +class GetInclusionListResponse(container) + inclusion_list_summary: InclusionListSummary + transactions: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] +``` + +```python +def get_execution_inclusion_list(self: ExecutionEngine, parent_block_hash: Root) -> GetInclusionListResponse: + """ + Return ``GetInclusionListResponse`` object. + """ + ... +``` + +## Beacon chain responsibilities + +All validator responsibilities remain unchanged other than those noted below. Namely, proposer normal block production switched to a new `BeaconBlockBody`. +Proposer with additional duty to construct and broadcast `InclusionList` alongside `SignedBeaconBlock`. +Attester with an additional duty to be part of PTC committee and broadcast `PayloadAttestationMessage`. + +## Validator assignment + +A validator can get PTC committee 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[Tuple[Sequence[ValidatorIndex], 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) + committee_count_per_slot = get_committee_count_per_slot(state, epoch) + for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH): + for index in range(committee_count_per_slot): + committee = get_ptc(state, Slot(slot)) + if validator_index in committee: + return committee, Slot(slot) + return None +``` + +### Lookahead + +The beacon chain shufflings are designed to provide a minimum of 1 epoch lookahead +on the validator's upcoming ptc assignments for attesting dictated by the shuffling and slot. + +[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. + +[Modified in MaxEB] + +a validator should: +* Find peers of the pubsub topic `beacon_attestation_{subnet_id}`. + * If the validator is assigned to be an aggregator for the slot (see `is_aggregator()`)[Modified in MaxEB], then subscribe to the topic. + +### Inclusion list proposal + +ePBS introduces forward inclusion list. The detail design is described in this [post](https://ethresear.ch/t/no-free-lunch-a-new-inclusion-list-design/16389) +Proposer must construct and broadcast `InclusionList` alongside `SignedBeaconBlock`. +- Proposer for slot `N` submits `SignedBeaconBlock` and in parallel submits `InclusionList` to be included at the beginning of slot `N+1` builder. +- Within `InclusionList`, `Transactions` are list of transactions that the proposer wants to include at the beginning of slot `N+1` builder. +- Within `inclusionList`, `Summaries` are lists consisting on addresses sending those transactions and their gas limits. The summaries are signed by the proposer `N`. +- Proposer may send many of these pairs that aren't committed to its beacon block so no double proposing slashing is involved. + +#### Constructing the inclusion list + +To obtain an inclusion list, a block proposer building a block on top of a `state` must take the following actions: + +1. Check if the previous slot is skipped. If `state.latest_execution_payload_header.time_stamp` is from previous slot. + * If it's skipped, the proposer should not propose an inclusion list. It can ignore rest of the steps. + +2. Retrieve inclusion list from execution layer by calling `get_execution_inclusion_list`. + +3. Call `build_inclusion_list` to build `InclusionList`. + +```python +def build_inclusion_list(state: BeaconState, inclusion_list_response: GetInclusionListResponse, block_slot: Slot, privkey: int) -> InclusionList: + inclusion_list_summary = inclusion_list_response.inclusion_list_summary + signature = get_inclusion_list_summary_signature(state, inclusion_list_summary, block_slot, privkey) + signed_inclusion_list_summary = SignedInclusionListSummary(summary=inclusion_list_summary, signature=signature) + return InclusionList(summaries=signed_inclusion_list_summary, transactions=inclusion_list_response.transactions) +``` + +In order to get inclusion list summary signature, the proposer will call `get_inclusion_list_summary_signature`. + +```python +def get_inclusion_list_summary_signature(state: BeaconState, inclusion_list_summary: InclusionListSummary, block_slot: Slot, privkey: int) -> BLSSignature: + domain = get_domain(state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(block_slot)) + signing_root = compute_signing_root(inclusion_list_summary, domain) + return bls.Sign(privkey, signing_root) +``` + +#### Broadcast inclusion list + +Finally, the proposer broadcasts `inclusion_list` to the inclusion list subnet, the `inclusion_list` pubsub topic. + +### Block proposal + +#### Constructing the new `signed_execution_payload_header_envelope` field in `BeaconBlockBody` + +To obtain `signed_execution_payload_header_envelope`, a block proposer building a block on top of a `state` must take the following actions: +* Listen to the `execution_payload_header_envelope` gossip subnet and save accepted `signed_execution_payload_header_envelope` from the builders. +* Filter out the header envelops where `signed_execution_payload_header_envelope.message.header.parent_hash` matches `state.latest_execution_payload_header.block_hash` +* The `signed_execution_payload_header_envelope` must satisfy the verification conditions found in `process_execution_payload_header` using `state` advance to the latest slot. +* Select the one bid and set `body.signed_execution_payload_header_envelope = signed_execution_payload_header_envelope` + +#### Constructing the new `payload_attestations` field in `BeaconBlockBody` + +Up to `MAX_PAYLOAD_ATTESTATIONS`, aggregate payload attestations can be included in the block. +The payload attestations added must satisfy the verification conditions found in payload attestation processing. It must pass `process_payload_attestation`. +`payload_attestations` can only be included in the next slot, so there's only a maximum of two possible aggregates that are valid. + +Some validators are selected to locally aggregate attestations with a similar `attestation_data` to their constructed `attestation` for the assigned `slot`. + +#### Aggregation selection + +A validator is selected to aggregate based upon the return value of `is_aggregator()`. [Modified in ePBS]. Taken from [PR](https://github.com/michaelneuder/consensus-specs/pull/3) + +```python +def is_aggregator(state: BeaconState, slot: Slot, index: CommitteeIndex, validator_index: ValidatorIndex, slot_signature: BLSSignature) -> bool: + validator = state.validators[validator_index] + committee = get_beacon_committee(state, slot, index) + min_balance_increments = validator.effective_balance // MIN_ACTIVATION_BALANCE + committee_balance_increments = get_total_balance(state, set(committee)) // MIN_ACTIVATION_BALANCE + denominator = committee_balance_increments ** min_balance_increments + numerator = denominator - (committee_balance_increments - TARGET_AGGREGATORS_PER_COMMITTEE) ** min_balance_increments + modulo = denominator // numerator + return bytes_to_uint64(hash(slot_signature)[0:8]) % modulo == 0 +``` + +Rest of the aggregation process remains unchanged. + +### Payload timeliness attestation + +Some validators are selected to submit payload timeliness attestation. The assigned `slot` for which the validator performs this role during an epoch are defined by `get_ptc(state, slot)`. + +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 payload attestation + +##### Prepare payload attestation message +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. + +```python +def is_assigned_to_payload_committee(state: BeaconState, + slot: Slot, + validator_index: ValidatorIndex) -> bool: + committe = get_ptc(state, slot) + return validator_index in committee +``` + +Next, the validator creates `payload_attestation_message` as follows: +* Set `payload_attestation_data.slot = slot` where `slot` is the assigned slot. +* Set `payload_attestation_data.beacon_block_root = block_root` where `block_root` is the head of the chain. +* Set `payload_attestation_data.payload_revealed = True` if the `SignedExecutionPayloadEnvelope` is seen from the block builder reveal at `SECONDS_PER_SLOT * 2 / INTERVALS_PER_SLOT`, and if `ExecutionPayloadEnvelope.beacon_block_root` matches `block_root` + * Otherwise, set `payload_attestation_data.payload_revealed = False`. +* 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. +* Set `payload_attestation_message = PayloadAttestationMessage(data=payload_attestation_data, signature=payload_attestation_signature)`, where `payload_attestation_signature` is obtained from: + +```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.slot)) + signing_root = compute_signing_root(attestation, domain) + return bls.Sign(privkey, signing_root) +``` + +#### Broadcast payload attestation + +Finally, the validator broadcasts `payload_attestation_message` to the global `payload_attestation_message` pubsub topic. + +### Attesting + +If you are assigned to PTC `is_assigned_to_payload_committee(state, slot, validtor_index)==true`, then you can skip attesting at `slot`. +The attestation will not be gaining any rewards and will be dropped on the gossip network. + +### Attestation aggregation + +Even if you skip attesting because of PTC, you should still aggregate attestations for the assigned slot. if `is_aggregator==true`. This is the honest behavior. + +## Design Rationale + +### What is the honest behavior to build on top of a skip slot for inclusion list? +The proposer shouldn't propose an inclusion list on top of a skip slot. +If the payload for block N isn't revealed, the summaries and transactions for slot N-1 remain valid. +The slot N+1 proposer can't submit a new IL, and any attempt will be ignored. +The builder for N+1 must adhere to the N-1 summary. +If k consecutive slots lack payloads, the next full slot must still follow the N-1 inclusion list. + +### Why skip the attestation if you are assigned to PTC? + +PTC validators are selected as the first index from each beacon committee, excluding builders. +These validators receive a full beacon attestation reward when they correctly identify the payload reveal status. +Specifically, if they vote for "full" and the payload is included, or vote for "empty" and the payload is excluded. +Attestations directed at the CL block from these validators are disregarded, eliminating the need for broadcasting. +This does not apply if you are an aggregator. \ No newline at end of file From ba51dcc617f22ce7964d0260c4a3ed5a556eabce Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 3 Apr 2024 11:10:18 -0300 Subject: [PATCH 061/112] deal with withdrawals in two steps --- specs/_features/epbs/beacon-chain.md | 40 +++++++++++++++++++++++++--- specs/_features/epbs/validator.md | 25 +++++------------ 2 files changed, 42 insertions(+), 23 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 3dcb2d9544..ac500c6209 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -277,7 +277,7 @@ class ExecutionPayloadHeader(Container): ``` #### `BeaconState` -*Note*: the beacon state is modified to store a signed latest execution payload header. 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. +*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): @@ -328,6 +328,7 @@ class BeaconState(Container): latest_block_hash: Hash32 # [New in ePBS] latest_full_slot: Slot # [New in ePBS] signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] + last_withdrawals_root: Root # [New in ePBS] ``` ## Helper functions @@ -445,7 +446,7 @@ The post-state corresponding to a pre-state `state` and a signed execution paylo ```python def process_block(state: BeaconState, block: BeaconBlock) -> None: process_block_header(state, block) # - # removed process_withdrawals(state, block.body.execution_payload) [Removed in ePBS] + removed 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) @@ -453,6 +454,37 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None: 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 !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 @@ -627,8 +659,8 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti assert verify_execution_envelope_signature(state, signed_envelope) envelope = signed_envelope.message payload = envelope.payload - # Process withdrawals - process_withdrawals(state, payload) + # Verify the withdrawals root + assert hash_tree_root(payload.withdrawals) == state.last_withdrawals_root # Verify inclusion list proposer proposer_index = envelope.inclusion_list_proposer_index assert proposer_index == state.previous_inclusion_list_proposer diff --git a/specs/_features/epbs/validator.md b/specs/_features/epbs/validator.md index b345b808fc..d01ba93ecb 100644 --- a/specs/_features/epbs/validator.md +++ b/specs/_features/epbs/validator.md @@ -1,3 +1,7 @@ +# 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)* @@ -30,30 +34,13 @@ -# ePBS -- Honest Validator - -**Notice**: This document is a work-in-progress for researchers and implementers. - -## Introduction - -This document represents the changes to be made in the code of an "honest validator" to implement ePBS. - -## Prerequisites - -This document is an extension of the Deneb -- Honest Validator guide. -All behaviors and definitions defined in this document, and documents it extends, carry over unless explicitly noted or overridden. - -All terminology, constants, functions, and protocol mechanics defined in the updated Beacon Chain doc of ePBS. are requisite for this document and used throughout. -Please see related Beacon Chain doc before continuing and use them as a reference throughout. - ## Protocols ### `ExecutionEngine` *Note*: `get_execution_inclusion_list` function is added to the `ExecutionEngine` protocol for use as a validator. -The body of this function is implementation dependent. -The Engine API may be used to implement it with an external execution engine. +The body of this function is implementation dependent. The Engine API may be used to implement it with an external execution engine. #### `get_execution_inclusion_list` @@ -269,4 +256,4 @@ PTC validators are selected as the first index from each beacon committee, exclu These validators receive a full beacon attestation reward when they correctly identify the payload reveal status. Specifically, if they vote for "full" and the payload is included, or vote for "empty" and the payload is excluded. Attestations directed at the CL block from these validators are disregarded, eliminating the need for broadcasting. -This does not apply if you are an aggregator. \ No newline at end of file +This does not apply if you are an aggregator. From 6e03e58da4a1f938ad64b16efffed02ecac7ace7 Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 3 Apr 2024 14:25:59 -0300 Subject: [PATCH 062/112] more p2p --- specs/_features/epbs/beacon-chain.md | 6 +- specs/_features/epbs/p2p-interface.md | 2 +- specs/_features/epbs/validator.md | 126 ++++++++++---------------- 3 files changed, 51 insertions(+), 83 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index ac500c6209..7a12b9cec9 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -268,7 +268,7 @@ class ExecutionPayload(Container): ```python class ExecutionPayloadHeader(Container): - parent_block_root: Root + parent_block_hash: Hash32 block_hash: Hash32 builder_index: ValidatorIndex slot: Slot @@ -513,7 +513,7 @@ def process_execution_payload_header(state: BeaconState, block: BeaconBlock) -> # 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_root = block.parent_root + assert header.parent_block_hash = state.latest_block_hash # Transfer the funds from the builder to the proposer decrease_balance(state, builder_index, amount) @@ -646,7 +646,7 @@ def verify_execution_payload_envelope_signature(state: BeaconState, signed_envel 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[message.proposer_index] + proposer = state.validators[summary.proposer_index] return bls.Verify(proposer.pubkey, signing_root, signed_summary.signature) ``` diff --git a/specs/_features/epbs/p2p-interface.md b/specs/_features/epbs/p2p-interface.md index 6fd830a164..84f63d1187 100644 --- a/specs/_features/epbs/p2p-interface.md +++ b/specs/_features/epbs/p2p-interface.md @@ -146,7 +146,7 @@ The following validations MUST pass before forwarding the `signed_execution_payl - _[IGNORE]_ this is the first signed bid seen with a valid signature from the given builder for this slot. - _[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_root` is a known block root in fork choice. +- _[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`. diff --git a/specs/_features/epbs/validator.md b/specs/_features/epbs/validator.md index d01ba93ecb..2c4d6d0496 100644 --- a/specs/_features/epbs/validator.md +++ b/specs/_features/epbs/validator.md @@ -38,17 +38,13 @@ This document represents the changes and additions to the Honest validator guide ### `ExecutionEngine` -*Note*: `get_execution_inclusion_list` function is added to the `ExecutionEngine` protocol for use as a validator. - -The body of this function is implementation dependent. The Engine API may be used to implement it with an external execution engine. - #### `get_execution_inclusion_list` -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. +*Note*: `get_execution_inclusion_list` function is added to the `ExecutionEngine` protocol for use as a validator. ```python class GetInclusionListResponse(container) - inclusion_list_summary: InclusionListSummary + summary: List[ExecutionAddress, MAX_TRANSACTIONS_PER_INCLUSION_LIST] transactions: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] ``` @@ -60,15 +56,16 @@ def get_execution_inclusion_list(self: ExecutionEngine, parent_block_hash: Root) ... ``` -## Beacon chain responsibilities +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. -All validator responsibilities remain unchanged other than those noted below. Namely, proposer normal block production switched to a new `BeaconBlockBody`. -Proposer with additional duty to construct and broadcast `InclusionList` alongside `SignedBeaconBlock`. -Attester with an additional duty to be part of PTC committee and broadcast `PayloadAttestationMessage`. +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 committee assignments for a given slot using the following helper via `get_ptc_assignment(state, epoch, validator_index)` where `epoch <= next_epoch`. +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. @@ -77,7 +74,7 @@ PTC committee selection is only stable within the context of the current and nex def get_ptc_assignment(state: BeaconState, epoch: Epoch, validator_index: ValidatorIndex - ) -> Optional[Tuple[Sequence[ValidatorIndex], Slot]]: + ) -> Optional[Slot]: """ Return the ptc committee assignment in the ``slot`` for ``validator_index``. ``assignment`` returned is a tuple of the following form: @@ -89,108 +86,79 @@ def get_ptc_assignment(state: BeaconState, assert epoch <= next_epoch start_slot = compute_start_slot_at_epoch(epoch) - committee_count_per_slot = get_committee_count_per_slot(state, epoch) for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH): - for index in range(committee_count_per_slot): - committee = get_ptc(state, Slot(slot)) - if validator_index in committee: - return committee, Slot(slot) + if validator_index in get_ptc(state, Slot(slot)): + return Slot(slot) return None ``` ### Lookahead -The beacon chain shufflings are designed to provide a minimum of 1 epoch lookahead -on the validator's upcoming ptc assignments for attesting dictated by the shuffling and slot. - [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. -[Modified in MaxEB] +## Beacon chain responsibilities + +All validator responsibilities remain unchanged other than the following: -a validator should: -* Find peers of the pubsub topic `beacon_attestation_{subnet_id}`. - * If the validator is assigned to be an aggregator for the slot (see `is_aggregator()`)[Modified in MaxEB], then subscribe to the topic. +- 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 -### Inclusion list proposal -ePBS introduces forward inclusion list. The detail design is described in this [post](https://ethresear.ch/t/no-free-lunch-a-new-inclusion-list-design/16389) -Proposer must construct and broadcast `InclusionList` alongside `SignedBeaconBlock`. -- Proposer for slot `N` submits `SignedBeaconBlock` and in parallel submits `InclusionList` to be included at the beginning of slot `N+1` builder. -- Within `InclusionList`, `Transactions` are list of transactions that the proposer wants to include at the beginning of slot `N+1` builder. -- Within `inclusionList`, `Summaries` are lists consisting on addresses sending those transactions and their gas limits. The summaries are signed by the proposer `N`. -- Proposer may send many of these pairs that aren't committed to its beacon block so no double proposing slashing is involved. +### Block proposal -#### Constructing the inclusion list +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. Check if the previous slot is skipped. If `state.latest_execution_payload_header.time_stamp` is from previous slot. - * If it's skipped, the proposer should not propose an inclusion list. It can ignore rest of the steps. +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 build_inclusion_list(state: BeaconState, inclusion_list_response: GetInclusionListResponse, block_slot: Slot, privkey: int) -> InclusionList: - inclusion_list_summary = inclusion_list_response.inclusion_list_summary - signature = get_inclusion_list_summary_signature(state, inclusion_list_summary, block_slot, privkey) - signed_inclusion_list_summary = SignedInclusionListSummary(summary=inclusion_list_summary, signature=signature) - return InclusionList(summaries=signed_inclusion_list_summary, transactions=inclusion_list_response.transactions) -``` - -In order to get inclusion list summary signature, the proposer will call `get_inclusion_list_summary_signature`. - -```python -def get_inclusion_list_summary_signature(state: BeaconState, inclusion_list_summary: InclusionListSummary, block_slot: Slot, privkey: int) -> BLSSignature: - domain = get_domain(state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(block_slot)) +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) ``` -#### Broadcast inclusion list - -Finally, the proposer broadcasts `inclusion_list` to the inclusion list subnet, the `inclusion_list` pubsub topic. - -### Block proposal +```python +def build_inclusion_list(state: BeaconState, inclusion_list_response: GetInclusionListResponse, block_slot: Slot, index: ValidatorIndex, privkey: int) -> InclusionList: + summary = InclusionListSummary(proposer_index=index, 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, slot=block_slot, transactions=inclusion_list_response.transactions) +``` #### Constructing the new `signed_execution_payload_header_envelope` field in `BeaconBlockBody` -To obtain `signed_execution_payload_header_envelope`, a block proposer building a block on top of a `state` must take the following actions: -* Listen to the `execution_payload_header_envelope` gossip subnet and save accepted `signed_execution_payload_header_envelope` from the builders. -* Filter out the header envelops where `signed_execution_payload_header_envelope.message.header.parent_hash` matches `state.latest_execution_payload_header.block_hash` -* The `signed_execution_payload_header_envelope` must satisfy the verification conditions found in `process_execution_payload_header` using `state` advance to the latest slot. -* Select the one bid and set `body.signed_execution_payload_header_envelope = signed_execution_payload_header_envelope` +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 payload attestations added must satisfy the verification conditions found in payload attestation processing. It must pass `process_payload_attestation`. -`payload_attestations` can only be included in the next slot, so there's only a maximum of two possible aggregates that are valid. - -Some validators are selected to locally aggregate attestations with a similar `attestation_data` to their constructed `attestation` for the assigned `slot`. - -#### Aggregation selection - -A validator is selected to aggregate based upon the return value of `is_aggregator()`. [Modified in ePBS]. Taken from [PR](https://github.com/michaelneuder/consensus-specs/pull/3) - -```python -def is_aggregator(state: BeaconState, slot: Slot, index: CommitteeIndex, validator_index: ValidatorIndex, slot_signature: BLSSignature) -> bool: - validator = state.validators[validator_index] - committee = get_beacon_committee(state, slot, index) - min_balance_increments = validator.effective_balance // MIN_ACTIVATION_BALANCE - committee_balance_increments = get_total_balance(state, set(committee)) // MIN_ACTIVATION_BALANCE - denominator = committee_balance_increments ** min_balance_increments - numerator = denominator - (committee_balance_increments - TARGET_AGGREGATORS_PER_COMMITTEE) ** min_balance_increments - modulo = denominator // numerator - return bytes_to_uint64(hash(slot_signature)[0:8]) % modulo == 0 -``` +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 aggregated signature of the payload attestation verifies correctly. -Rest of the aggregation process remains unchanged. ### Payload timeliness attestation @@ -234,7 +202,7 @@ Finally, the validator broadcasts `payload_attestation_message` to the global `p ### Attesting -If you are assigned to PTC `is_assigned_to_payload_committee(state, slot, validtor_index)==true`, then you can skip attesting at `slot`. +Validators are assigned to PTC `is_assigned_to_payload_committee(state, slot, validtor_index)==true`, then you can skip attesting at `slot`. The attestation will not be gaining any rewards and will be dropped on the gossip network. ### Attestation aggregation From d89e527089ab998d8722298329a74d35c9862a3d Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 3 Apr 2024 16:10:39 -0300 Subject: [PATCH 063/112] deal with ptc equivocations --- specs/_features/epbs/beacon-chain.md | 26 +++++++++-- specs/_features/epbs/validator.md | 69 ++++++++-------------------- 2 files changed, 40 insertions(+), 55 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 7a12b9cec9..0497829a7d 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -594,6 +594,12 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None: ##### Payload Attestations +```python +def remove_flag(flags: ParticipationFlags, flag_index: int) -> ParticipationFlags: + flag = PartitipationFlags(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 @@ -615,20 +621,32 @@ def process_payload_attestation(state: BeaconState, payload_attestation: Payload # Return early if the attestation is for the wrong payload status payload_was_present = data.slot == state.latest_full_slot voted_preset = 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_atterstation.attesting_indices: + for flag_index, weight in enumerate(PARTICIPATION_FLAG_WEIGHTS): + if has_flag(epoch_participation[index], flag_index): + epoch_participation[index] = remove_flag(flag_index) + proposer_penalty_numerator += get_base_reward(state, index) * weight + # Penalize the proposer + proposer_penalty = Gwei(proposer_penalty_numerator // proposer_reward_denominator) + decrease_balance(state, proposer_index, proposer_penalty) return - # Reward the proposer and set all the participation flags + + # 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 + proposer_reward_numerator += base_reward * 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) + increase_balance(state, proposer_index, proposer_reward) ``` #### New `verify_execution_payload_envelope_signature` diff --git a/specs/_features/epbs/validator.md b/specs/_features/epbs/validator.md index 2c4d6d0496..303c922a25 100644 --- a/specs/_features/epbs/validator.md +++ b/specs/_features/epbs/validator.md @@ -157,71 +157,38 @@ Up to `MAX_PAYLOAD_ATTESTATIONS`, aggregate payload attestations can be included * 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 aggregated signature of the payload attestation verifies correctly. - + - 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. ### Payload timeliness attestation -Some validators are selected to submit payload timeliness attestation. The assigned `slot` for which the validator performs this role during an epoch are defined by `get_ptc(state, slot)`. +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 payload attestation +#### Constructing a payload attestation -##### Prepare payload attestation message 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. -```python -def is_assigned_to_payload_committee(state: BeaconState, - slot: Slot, - validator_index: ValidatorIndex) -> bool: - committe = get_ptc(state, slot) - return validator_index in committee -``` - -Next, the validator creates `payload_attestation_message` as follows: -* Set `payload_attestation_data.slot = slot` where `slot` is the assigned slot. -* Set `payload_attestation_data.beacon_block_root = block_root` where `block_root` is the head of the chain. -* Set `payload_attestation_data.payload_revealed = True` if the `SignedExecutionPayloadEnvelope` is seen from the block builder reveal at `SECONDS_PER_SLOT * 2 / INTERVALS_PER_SLOT`, and if `ExecutionPayloadEnvelope.beacon_block_root` matches `block_root` - * Otherwise, set `payload_attestation_data.payload_revealed = False`. +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. -* Set `payload_attestation_message = PayloadAttestationMessage(data=payload_attestation_data, signature=payload_attestation_signature)`, where `payload_attestation_signature` is obtained from: +* 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.slot)) - signing_root = compute_signing_root(attestation, domain) + 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) ``` - -#### Broadcast payload attestation - -Finally, the validator broadcasts `payload_attestation_message` to the global `payload_attestation_message` pubsub topic. - -### Attesting - -Validators are assigned to PTC `is_assigned_to_payload_committee(state, slot, validtor_index)==true`, then you can skip attesting at `slot`. -The attestation will not be gaining any rewards and will be dropped on the gossip network. - -### Attestation aggregation - -Even if you skip attesting because of PTC, you should still aggregate attestations for the assigned slot. if `is_aggregator==true`. This is the honest behavior. - -## Design Rationale - -### What is the honest behavior to build on top of a skip slot for inclusion list? -The proposer shouldn't propose an inclusion list on top of a skip slot. -If the payload for block N isn't revealed, the summaries and transactions for slot N-1 remain valid. -The slot N+1 proposer can't submit a new IL, and any attempt will be ignored. -The builder for N+1 must adhere to the N-1 summary. -If k consecutive slots lack payloads, the next full slot must still follow the N-1 inclusion list. - -### Why skip the attestation if you are assigned to PTC? - -PTC validators are selected as the first index from each beacon committee, excluding builders. -These validators receive a full beacon attestation reward when they correctly identify the payload reveal status. -Specifically, if they vote for "full" and the payload is included, or vote for "empty" and the payload is excluded. -Attestations directed at the CL block from these validators are disregarded, eliminating the need for broadcasting. -This does not apply if you are an aggregator. From 7c5b4589a9d1ee11994a78f6153b6f6a902e076c Mon Sep 17 00:00:00 2001 From: Potuz Date: Thu, 4 Apr 2024 08:13:16 -0300 Subject: [PATCH 064/112] builder init --- specs/_features/epbs/builder.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 specs/_features/epbs/builder.md diff --git a/specs/_features/epbs/builder.md b/specs/_features/epbs/builder.md new file mode 100644 index 0000000000..e45b5c236c --- /dev/null +++ b/specs/_features/epbs/builder.md @@ -0,0 +1,26 @@ +# 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 + +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 to reveal a full 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. + +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. Construct an execution payload. This can be performed with an external execution engine with a call to Set `header.block_hash` for t From 7198b11ef6a34525fff6ab0d0dfa9b469648a3c3 Mon Sep 17 00:00:00 2001 From: Potuz Date: Thu, 4 Apr 2024 14:27:33 -0300 Subject: [PATCH 065/112] Some hardening - Add slot and parent hash to the IL, slot to harden against replays, hash is not signed and is sent for convenience to the builder - Add last slot with an inclusion list and the previous slot with an IL to the beacon state so that it can be enforced that the payload includes the right inclusion list to be satisfied. --- specs/_features/epbs/beacon-chain.md | 27 ++++++++++----- specs/_features/epbs/builder.md | 50 ++++++++++++++++++++++++++- specs/_features/epbs/p2p-interface.md | 7 ++-- 3 files changed, 71 insertions(+), 13 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 0497829a7d..5f4cf94141 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -169,6 +169,7 @@ class ExecutionPayloadEnvelope(Container): 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 @@ -187,6 +188,7 @@ class SignedExecutionPayloadEnvelope(Container): ```python class InclusionListSummary(Container) proposer_index: ValidatorIndex + slot: Slot summary: List[ExecutionAddress, MAX_TRANSACTIONS_PER_INCLUSION_LIST] ``` @@ -195,6 +197,7 @@ class InclusionListSummary(Container) ```python class SignedInclusionListSummary(Container): message: InclusionListSummary + parent_block_hash: Hash32 signature: BLSSignature ``` @@ -203,7 +206,6 @@ class SignedInclusionListSummary(Container): ```python class InclusionList(Container) signed_summary: SignedInclusionListSummary - slot: Slot transactions: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] ``` @@ -324,7 +326,9 @@ class BeaconState(Container): 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] signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] @@ -522,7 +526,7 @@ def process_execution_payload_header(state: BeaconState, block: BeaconBlock) -> # 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.signed_execution_payload_header = signed_header ``` @@ -672,20 +676,23 @@ def verify_inclusion_list_summary_signature(state: BeaconState, signed_summary: *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) -> None: +def process_execution_payload(state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope, execution_engine: ExecutionEngine, verify = True) -> None: # Verify signature - assert verify_execution_envelope_signature(state, signed_envelope) + if verify: + assert verify_execution_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 - proposer_index = envelope.inclusion_list_proposer_index - assert proposer_index == state.previous_inclusion_list_proposer + # 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 + assert payload. # Verify inclusion list summary signature signed_summary = SignedInclusionListSummary( message=InclusionListSummary( - proposer_index=proposer_index + 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) @@ -715,6 +722,8 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti 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 - assert envelope.state_root == hash_tree_root(state) + if verify: + assert envelope.state_root == hash_tree_root(state) ``` diff --git a/specs/_features/epbs/builder.md b/specs/_features/epbs/builder.md index e45b5c236c..b816851864 100644 --- a/specs/_features/epbs/builder.md +++ b/specs/_features/epbs/builder.md @@ -22,5 +22,53 @@ Thus, builders tasks are divided in two, submitting bids, and submitting payload 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. Construct an execution payload. This can be performed with an external execution engine with a call to Set `header.block_hash` for t +2. Construct an execution payload. This can be performed with an external execution engine with a call to `engine_getPayloadV4`. +3. Set `header.block_hash` to be the block hash of the constructed payload, that is `payload.block_hash` +4. Set `header.builder_index` to be the validator index of the builder performing these actions. +5. 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. +6. 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. +7. 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 execution payload envelope + +When a valid `SignedBeaconBlock` has been published containing a signed commitment by the builder, the builder is later expected to broadcast the corresponding `SignedExecutionPayloadEnvelope` that fulfils 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 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/p2p-interface.md b/specs/_features/epbs/p2p-interface.md index 84f63d1187..3286235e22 100644 --- a/specs/_features/epbs/p2p-interface.md +++ b/specs/_features/epbs/p2p-interface.md @@ -116,13 +116,13 @@ This topic is used to propagate execution payload messages as `SignedExecutionPa 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` - _[REJECT]_ `payload.block_hash == header.block_hash` -- _[REJECT]_ `hash_tree_root(payload.blob_kzg_commitments) == header.blob_kzg_commitments_root` - _[REJECT]_ The builder signature, `signed_execution_payload_envelope.signature`, is valid with respect to the builder's public key. ###### `payload_attestation_message` @@ -133,6 +133,7 @@ The following validations MUST pass before forwarding the `payload_attestation_m - _[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 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. @@ -157,7 +158,7 @@ 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 inclusion list with valid signature received for the proposer for the slot, `inclusion_list.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. From 7adb9eaf97256242f722d9dcb0059bf8ff79b5bc Mon Sep 17 00:00:00 2001 From: Potuz Date: Thu, 4 Apr 2024 16:32:49 -0300 Subject: [PATCH 066/112] blob sidecar changes --- specs/_features/epbs/builder.md | 35 +++++++++++++++++++++++++++++++ specs/_features/epbs/validator.md | 3 +++ 2 files changed, 38 insertions(+) diff --git a/specs/_features/epbs/builder.md b/specs/_features/epbs/builder.md index b816851864..e7e16707b4 100644 --- a/specs/_features/epbs/builder.md +++ b/specs/_features/epbs/builder.md @@ -42,6 +42,41 @@ def get_execution_payload_header_signature(state: BeaconState, header: Execution 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 a valid `SignedBeaconBlock` has been published containing a signed commitment by the builder, the builder is later expected to broadcast the corresponding `SignedExecutionPayloadEnvelope` that fulfils this commitment. See below for a special case of an *honestly withheld payload*. diff --git a/specs/_features/epbs/validator.md b/specs/_features/epbs/validator.md index 303c922a25..dc206820af 100644 --- a/specs/_features/epbs/validator.md +++ b/specs/_features/epbs/validator.md @@ -161,6 +161,9 @@ Up to `MAX_PAYLOAD_ATTESTATIONS`, aggregate payload attestations can be included * 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. From 4160debfede78a66f08ce52e12e6302c0573bf05 Mon Sep 17 00:00:00 2001 From: Potuz Date: Thu, 4 Apr 2024 17:22:52 -0300 Subject: [PATCH 067/112] engine init --- specs/_features/epbs/engine.md | 223 +++++++++++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 specs/_features/epbs/engine.md diff --git a/specs/_features/epbs/engine.md b/specs/_features/epbs/engine.md new file mode 100644 index 0000000000..5788ceda1f --- /dev/null +++ b/specs/_features/epbs/engine.md @@ -0,0 +1,223 @@ +# Engine API -- ePBS + +Engine API changes introduced in the ePBS fork + +## Table of contents + + + + +- [Structures](#structures) + - [ExecutionPayloadV3](#executionpayloadv3) + - [BlobsBundleV1](#blobsbundlev1) + - [PayloadAttributesV3](#payloadattributesv3) +- [Methods](#methods) + - [engine_newPayloadV3](#engine_newpayloadv3) + - [Request](#request) + - [Response](#response) + - [Specification](#specification) + - [engine_forkchoiceUpdatedV3](#engine_forkchoiceupdatedv3) + - [Request](#request-1) + - [Response](#response-1) + - [Specification](#specification-1) + - [engine_getPayloadV3](#engine_getpayloadv3) + - [Request](#request-2) + - [Response](#response-2) + - [Specification](#specification-2) + - [Deprecate `engine_exchangeTransitionConfigurationV1`](#deprecate-engine_exchangetransitionconfigurationv1) + + + +## 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. +- `parent_block_hash`: `DATA`, 32 Bytes. +- `proposer_index` : `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). + +#### Respose + +- 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`. + +### Deprecate `engine_exchangeTransitionConfigurationV1` + +This document introduces deprecation of [`engine_exchangeTransitionConfigurationV1`](./paris.md#engine_exchangetransitionconfigurationv1). The deprecation is specified as follows: + +1. Consensus layer clients **MUST NOT** call this method. + +2. Execution layer clients **MUST NOT** surface an error message to the user if this method is not called. + +3. Consensus and execution layer clients **MAY** remove support of this method after Cancun. If no longer supported, this method **MUST** be removed from the [`engine_exchangeCapabilities`](./common.md#engine_exchangecapabilities) request or response list depending on whether it is consensus or execution layer client. + +### Update the methods of previous forks + +This document defines how Cancun payload should be handled by the [`Shanghai API`](./shanghai.md). + +For the following methods: + +- [`engine_forkchoiceUpdatedV2`](./shanghai.md#engine_forkchoiceupdatedv2) +- [`engine_newPayloadV2`](./shanghai.md#engine_newpayloadV2) +- [`engine_getPayloadV2`](./shanghai.md#engine_getpayloadv2) + +a validation **MUST** be added: + +1. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of payload or payloadAttributes greater or equal to the Cancun activation timestamp. From 77cd6aa9fdd44be823794aecfb103e1245e91dfa Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 5 Apr 2024 15:06:26 -0300 Subject: [PATCH 068/112] doctoc --- specs/_features/epbs/beacon-chain.md | 1 + specs/_features/epbs/builder.md | 6 + specs/_features/epbs/design.md | 204 --------------------------- specs/_features/epbs/engine.md | 18 ++- specs/_features/epbs/validator.md | 38 ++--- 5 files changed, 32 insertions(+), 235 deletions(-) delete mode 100644 specs/_features/epbs/design.md diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 5f4cf94141..71e6381d9c 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -44,6 +44,7 @@ - [`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) diff --git a/specs/_features/epbs/builder.md b/specs/_features/epbs/builder.md index e7e16707b4..751002d20b 100644 --- a/specs/_features/epbs/builder.md +++ b/specs/_features/epbs/builder.md @@ -5,6 +5,12 @@ This is an accompanying document which describes the expected actions of a "buil +- [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) diff --git a/specs/_features/epbs/design.md b/specs/_features/epbs/design.md deleted file mode 100644 index 4609400011..0000000000 --- a/specs/_features/epbs/design.md +++ /dev/null @@ -1,204 +0,0 @@ - - -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [ePBS design notes](#epbs-design-notes) - - [Inclusion lists](#inclusion-lists) - - [Liveness](#liveness) - - [Censoring](#censoring) - - [Builders](#builders) - - [Builder Payments](#builder-payments) - - [Withdrawals](#withdrawals) - - [Blobs](#blobs) - - [PTC Rewards](#ptc-rewards) - - [PTC Attestations](#ptc-attestations) - - [Forkchoice changes](#forkchoice-changes) - - [Checkpoint states](#checkpoint-states) - - [Block slot](#block-slot) - - [Equivocations](#equivocations) - - [Increased Max EB](#increased-max-eb) - - [Validator transfers](#validator-transfers) - - - -# ePBS design notes - -## Inclusion lists - -ePBS introduces forward inclusion lists for proposers to guarantee censor resistanship of the network. We follow the design described in [this post](https://ethresear.ch/t/no-free-lunch-a-new-inclusion-list-design/16389). - -- Proposer for slot N submits a signed block and in parallel broadcasts pairs of `summaries` and `transactions` to be included at the beginning of slot N+1. `transactions` are just list of transactions that this proposer wants included at the most at the beginning of N+1. `Summaries` are lists consisting on addresses sending those transactions and their gas limits. The summaries are signed, the transactions aren't. An honest proposer is allowed to send many of these pairs that aren't committed to its beacon block so no double proposing slashing is involved. -- Validators for slot N will consider the block for validation only if they have seen at least one pair (summary, transactions). They will consider the block invalid if those transactions are not executable at the start of slot N and if they don't have at least 12.5% higher `maxFeePerGas` than the current slot's `maxFeePerGas`. -- The builder for slot N reveals its payload together with a signed summary of the proposer of slot N-1. Along the summary, the builder includes a list of transactions indices (in strictly increasing order) of the previous payload of slot N-1, that satisfy some entry in the signed inclusion list summary. The payload is considered only valid if the following applies - - For each index `i` in the payload's `inclusion_list_exclusions`, check that the ith transaction `tx[i]` of the payload for `N-1` satisfies some transaction `T[i]` of the current inclusion list. Here `T[i]` is the first entry in the payload's inclusion list that is satisfied by `tx[i]`. This `T[i]` is removed from the inclusion list summary. - - The remaining transactions in the inclusion list summary, are all satisfied by the first transactions in the current payload, in increasing order, starting from the first transaction. - - The payload is executable, that is, it's valid from the execution layer perspective. - -**Note:** in the event that the payload for the canonical block in slot N is not revealed, then the summaries and transactions list for slot N-1 remains valid, the honest proposer for slot N+1 is not allowed to submit a new IL and any such message will be ignored. The builder for N+1 still has to satisfy the summary of N-1. If there are k slots in a row that are missing payloads, the next full slot will still need to satisfy the inclusion list for N-1. - -### Liveness - -In the usual case of LMD+Ghost we have a proof of the *plausible liveness* theorem, that is that supermajority links can always be added to produce new finalized checkpoints provided there exist children extending the finalized chain. Here we prove that the next builder can always produce a valid payload, in particular, a payload that can satisfy the pending inclusion list. - -Let N be the last slot which contained a full execution payload, and let $N+1,..., N+k$, $k \geq 1$ be slots in the canonical chain, descending from $N$ that were either skipped or are *empty*, that is, the corresponding execution payload has not been revealed or hasn't been included. The consensus block for $N+k$ has been proposed and it is the canonical head. The builder for $N+k$ has to fulfill the inclusion list proposed by $N$. When importing the block $N$, validators have attested for availability of at least one valid inclusion list. That is, those transactions would be executable on top of the head block at the time. Let $P$ be the execution payload included by the builder of $N$, this is the current head payload. Transactions in the attested inclusion list can *only* be invalid in a child of $P$ if there are transactions in $P$ that have the same source address and gas usage that was below the gas limit in the summary. For any such transaction the builder can add such transaction to the exclusion list and not include it in its payload. If there are remaining transactions in its payload from the same address, the nonce will have to be higher nonce than the transaction that was added in the exclusion list. This process can be repeated until there are no more transactions in the summary from that given address that have been invalidated. - -### Censoring - -We prove the following: the builder cannot force a transaction in the inclusion list to revert due to gas limit usage. A malicious builder that attempts to add in the exclusion list some transactions from an address with high gas limit but low usage, so that the remaining transactions in the summary have lower gas limit and the included transaction with higher gas usage reverts. However, this is impossible since any transaction in the exclusion list has to have lower nonce since it was already included in the previous block. That is, any attempt by the builder of changing the order in which to include transactions in the exclusion list, will result in its payload being invalid, and thus the inclusion lists transactions that haven't been invalidated on N will remain valid for the next block. - -## Builders - -There is a new entity `Builder` that is a glorified validator (they are simply validators with a different withdrawal prefix `0x0b`) required to have a higher stake and required to sign when producing execution payloads. - -- Builders are also validators (otherwise their staked capital depreciates). -- There is nothing to be done to onboard builders as we can simply accept validators with the right `0x0b` withdrawal prefix before the fork. They will be builders automatically. We could however onboard builders by simply turning validators into builders if they achieve the necessary minimum balance and change their withdrawal prefix to be distinguished from normal validators at the fork. -- We need to include several changes from the [MaxEB PR](https://github.com/michaelneuder/consensus-specs/pull/3) in order to account with builders having an increased balance that would otherwise depreciate. - -## Builder Payments - -Payments are processed unconditionally when processing the signed execution payload header. There are cases to study for possible same-slot unbundling even by an equivocation. Same slot unbundling can happen if the proposer equivocates, and propagates his equivocation after seeing the reveal of the builder which happens at 8 seconds. The next proposer has to build on full which can only happen by being dishonest. Honest validators will vote for the previous block not letting the attack succeed. The honest builder does not lose his bid as the block is reorged. - -## Withdrawals - -Withdrawals are deterministic on the beacon state, so on a consensus layer block processing, they are immediately processed, then later when the payload appears we verify that the withdrawals in the payload agree with the already fulfilled withdrawals in the CL. - -So when importing the CL block for slot N, we process the expected withdrawals at that slot. We save the list of paid withdrawals to the beacon state. When the payload for slot N appears, we check that the withdrawals correspond to the saved withdrawals. If the payload does not appear, the saved withdrawals remain, so any future payload has to include those. - -## Blobs - -- KZG Commitments are now sent on the Execution Payload envelope broadcasted by the EL and the EL block can only be valid if the data is available. -- Blobs themselves may be broadcasted by the builder below as soon as it sees the beacon block if he sees it's safe. - -## PTC Rewards -- PTC members are obtained as the first members from each beacon slot committee that are not builders. -- attesters are rewarded as a full attestation when they get the right payload presence: that is, if they vote for full (resp. empty) and the payload is included (resp. not included) then they get their participation bits (target, source and head timely) set. Otherwise they get a penalty as a missed attestation. -- Attestations to the CL block from these members are just ignored. -- The proposer for slot N+1 must include PTC attestations for slot N. There is no rewards (and therefore no incentive) for the proposer to include attestations that voted incorrectly, perhaps we can simply accept 1 PTC attestation per block instead of the current two. - -## PTC Attestations - -There are two ways to import PTC attestations. CL blocks contain aggregates, called `PayloadAttestation` in the spec. And committee members broadcast unaggregated `PayloadAttestationMessage`s. The latter are only imported over the wire for the current slot, and the former are only imported on blocks for the previous slot. - -## Forkchoice changes - -There are significant design decisions to make due to the fact that a slot can have 3 different statuses: - -1. The CL block is not included (therefore no payload can be included). This is a *skipped* slot. -2. The CL block is included and the payload is not revealed. This is an *empty* slot. -3. The CL block and the payload are both included. This is a *full* slot. - -Consider the following fork -```mermaid -graph RL -A[N-1, Full] -B[N, Empty] --> A -C[N, Full] --> A -D[N+1, Full] --> B -``` - -In this fork the proposer of `N+1` is attempting to reorg the payload of `N` that was seen by the majority of the PTC. Suppose that honest validators see that the PTC has voted `N` to be full. Then because of proposer boost, the CL block of `N+1` will have 40% of the committee to start with. Assuming perfect participation, honest validators should see a weight of `100` for `(N, Full)` and a weight of `40` for `N+1` (notice that they attest before seeing the payload). They should choose to vote for `(N, Full)` instead of `N+1`. The question is how do we account for all of this? A few initial comments are in line -- CL attestation do not mention full or empty they simply have a beacon block root. Honest validators will have already set their PTC vote during `N` that `N` was full. -- The only changes to the view of `N` as empty/full could come only when importing `N+1`, a beacon block that contains PTC Messages attesting for the payload of `N`. However, if honest validators have already achieved the threshold for `full`, they will consider the block full. -- This is one design decision: instead of having a hard threshold on the PTC (50% in the current spec) we could have a relative one, say for example a simple majority of the counted votes. This has some minor engineering problems (like keeping track of who voted in forkchoice more than simply if they voted for present or not), but this can easily be changed if there are some liveness concerns. -- The honest builder for `N+1` would not submit a bid here, since honest builders would have seen `N` as full also, they would only build on top of the blockhash included in `N`. -- The honest PTC members for `N+1` will vote for `N, Full` they will be rewarded but they will not change the forkchoice view that `N` was already full. -- PTC members voting for a previous blockroot cannot change the forkchoice view of the payload status either way. - -So the questions is what changes do we need to make to our current weight accounting so that we have `(N, Full)` and `(N+1, Full)` as viable for head above, but not `(N, Empty)`?. Moreover, we want `(N, Full)` to be the winner in the above situation. Before dwelling in the justification, let me say right away that a proposer for `N+2` would call `get_head` and would get `N.root`. And then he will call `is_payload_present(N.root)` and he will get `True` so he will propose based on `(N, Full)` reorging back the dishonest (malinformed) proposer of `N+1`. The implementation of `is_payload_present` is trivial so the only question is how to do LMD counting so that `N` beats `N+1` in the head computation. - -There are several notions that can be changed when we have *empty* or *full* slots. -- Checkpoints: - - we can consider checkpoints to be of the form `(Epoch, Root, bool)` where the `bool` is to indicate if the Beacon block was full or not. - - Another option is to consider checkpoints to be of the form `(Epoch, Root)` exactly as we do today, but only consider the last *full* block before or equal to the `Epoch` start. -Both have advantages and disadvantages, the second one allows for different contending states to be the first state of an epoch, but it keeps all implementations exactly as they are today. - - A third approach, which seems the best so far, is to keep `(Epoch, Root)` and let head of the chain determine if it is *Full* or *Empty* as described below. -- Ancestry computations, as in `get_ancestor`. - - We can change the signature of this function to be of the form `get_ancestor(Store, Root, slot) -> (Root, bool)` So that it returns the beacon block root and weather or not it is based on *Full* or *Empty*. - - Otherwise we can simply return the last *Full* block in the line of ancestry. Again there are advantages and disadvantages. In this last case, it would be very hard to know if two given blocks with a given payload status are in the same chain or not. - - -The proposal I am considering at this moment is the following: -- Keep checkpoints as `(Epoch, Root) ` and allow different start of epoch blocks. -- An honest validator, when requesting the state at a given block root, will use its canonical state. That is computed as follows. In the example above, when requesting the state with block root `N`, if a call to `get_head` returned `N+1` then the validator would return the `store.block_states[N.root]` which corresponds to `N, Empty`. If instead returned `N` then it would return the state `store.execution_payload_states[N.root]` which corresponds to `N, Full`. -- Thus, when requesting the *justified state* for example, it will use the state that actually corresponds to its canonical chain and he needs to track only `Epoch, Root` for checkpoints, with minimal code changes. -- For LMD accounting, the proposal is to keep weights exactly as today with one exception: direct votes for `N` are *only* counted in the chains supporting `N, Full` or `N, Empty` according to the PTC vote. So, in the fork above, any honest validator that voted for `N` during slot `N` will be counted in the chain for `N, Full`, but not in the chain of `N+1, Full`. Honest validators during `N+1` will also vote for `N`, and also counting their votes in for `N, Full` and not the attacker's `N+1`. Suppose the chain advances with two more bad blocks as follows -```mermaid -graph RL -A[N-1, Full] -B[N, Empty] --> A -C[N, Full] --> A -D[N+1, Full] --> B -G[N+1, Empty] --> B -E[N+2, Full] --> D -F[N+3, Full] --> G -F ~~~ E -``` -In this case all the attesters for `N+1` will be counted depending on the PTC members that voted for `(N+1, Full)`. Assuming honest PTC members, they would have voted for `N` during `N+1` so any CL attesters for `N+1` would be voting for `N+1, Empty` thus only counting for the head in `(N+3, Full)`. - -### Checkpoint states -There is no current change in `store.checkpoint_states[root]`. In principle the "checkpoint state" should correspond to either the checkpoint block being full or empty. However, payload status does not change any consensus value for the state at the given time, so it does not matter if we continue using `store.block_states` which corresponds to the "empty" case. - -### Block slot - -Honest validators that vote for a parent block when a block is late, are contributing for this parent block support and are explicitly attesting that the current block is not present. This is taken into account in the new computation of `get_head`. Consider the following situation -```mermaid -graph RL -A[N-1, Full] -B[N, Full] --> A -``` -The block `N` has arrived late and the whole committee sees `A` as head and vote for `N-1`. At the start of `N+1` a call to `get_head` will return `N-1` as head and thus if the proposer of `N+1` is honest it will base its block on `N-1`. Suppose however that the proposer bases his block on top of `N`. Then we see -```mermaid -graph RL -A[N-1, Full] -B[N, Full] --> A -C[N+1, Full] --> B -``` -This block was timely so it gets proposer Boost. The real DAG is -```mermaid -graph RL -A[N-1, Full] -B[N, Full] --> A -C[N+1, Full] --> B -D[N-1, Full] --> A -E[N-1, Full] --> D -``` -And honest validators should still see `N-1` as head. The reason being that at the attestation deadline on `N+1` validators have seen block `N+1` appear, this block is valid and has 40% of a committee vote because of proposer boost. However, the branch for `N-1` has the full committee from `N` that has voted for it, and thus honest validators vote for `N-1` as valid. - -## Equivocations - -There is no need to do anything about proposer equivocations. Builders should reveal their block anyway. - -- At the time of reveal, the builder already has counted attestations for the current CL blocks, even if there are or not equivocations. Any equivocation available at this time will not have transactions that can unbundle him since he hasn't revealed. -- If the original block to which the builder committed is included, then the builder doesn't lose anything, that was the original intent. So if the original block is the overwhelming winner at the time of reveal, the builder can simply reveal and be safe that if there are any equivocations anyway his block was included. -- If the builder reveals, he knows that he can never be unbundled unless the next committee has a majority of malicious validators: attestations will go for an empty block before a block that is revealed after 8 seconds. -- So since the builder cannot be unbundled, then he either doesn't pay if the block is not included, or pays and its included. -- The splitting grief, that is, the proposer's block has about 50% of the vote at 8 seconds, remains. - -A little care has to be taken in the case of colluding proposers for `N` and `N+1`. Consider the example of the [previous section](#block-slot). The malicious proposer of `N` sends an early block to the builder and an equivocation after it has seen the payload. No honest validators will have voted for this equivocation. Suppose $\beta$ is the malicious stake. We have $1 - \beta$ for that votes for the early `N` as head and $\beta$ that will vote for the lately revealed block. Assuming $\beta < 0.5$ we have that the PTC committee will declare the equivocation as empty. The malicious proposer of `N+1` proposes based on the equivocating block `N` including some unbundled transactions. Because of the PTC vote, even the $\beta$ attestations for the equivocating block `N` will not count for `N+1` since it builds on *full* instead of empty. The weight of `N+1` is only given by proposer boost. The weight of the early `N` will be $1 - \beta$ thus beating the malicious `N+1` if $\beta < 0.6$ and thus honest validators will vote for the early `N` that included the builders' payload. However, the early block itself may cause a split view, in this case some attesters may have voted for `N-1` as head! in this situation we would have a DAG like this (we are not considering irrelevant branches) -```mermaid -graph RL -A[N-1, Full] -B[N, Full] --> A -H[N, Full] --> B -F[N', Full] --> A -I[N', Empty] --> A -C[N+1, Full] --> F -D[N-1, Full] --> A -E[N-1, Full] --> D -``` - -When recursing from the children of `N-1` the weights for the three children are as follows (when computing after `N+1` has been revealed and before validators for `N+1` attested) -- (N, Full) has gotten some vote $\gamma \leq 1 - \beta$. -- (N', Full) has zero weight. This is an important point. Proposer boost does not apply to it because even though $N+1$ will get proposer boost, it is based on the wrong `PTC` vote, and thus it does not count towards this node's weight. -- (N', Empty) has $\beta$ maximum. -- (N-1, Full) has $1 - \beta - \gamma$. - -Thus, supposing $\gamma < \beta$ we have that $1 - \beta - \gamma > 1 - 2 \beta > \beta$ as long as $\beta < 1/3$. Thus we are protected from these kinds of attacks from attackers up to 33%. - -Note however that if we were to apply proposer boost to `(N', Full)` then we see that there's a split now between the three possible heads. `N'` has proposer boost giving it $0.4$ so if $\gamma = 0.39$ we get that with $1 - \beta - \gamma < 0.4$ whenever $\beta \geq 0.2$. Thus a 20% attacker that can also split the network, would be able to carry this attack with two consecutive blocks. - -## Increased Max EB -This PR includes the changes from [this PR](https://github.com/michaelneuder/consensus-specs/pull/3). In particular it includes execution layer triggerable withdrawals. - -## Validator transfers -One of the main problems of the current design is that a builder can transfer arbitrary amounts to proposers by simply paying a large bid. This is dangerous from a forkchoice perspective as it moves weights from one branch to another instantaneously, it may prevent a large penalty in case of slashing, etc. In order to partially mitigate this, we churn the transfer overloading the deposit system of Max EB, that is we append a `PendingBalanceDeposit` object to the beacon state. This churns the increase in the proposer's balance while it discounts immediately the balance of the builder. We may want to revisit this and add also an exit churn and even deal with equivocations on future iterations. diff --git a/specs/_features/epbs/engine.md b/specs/_features/epbs/engine.md index 5788ceda1f..ee39e9ae48 100644 --- a/specs/_features/epbs/engine.md +++ b/specs/_features/epbs/engine.md @@ -8,23 +8,29 @@ Engine API changes introduced in the ePBS fork - [Structures](#structures) - - [ExecutionPayloadV3](#executionpayloadv3) - - [BlobsBundleV1](#blobsbundlev1) + - [ExecutionPayloadV4](#executionpayloadv4) - [PayloadAttributesV3](#payloadattributesv3) + - [InclusionListV1](#inclusionlistv1) + - [InclusionListStatusV1](#inclusionliststatusv1) - [Methods](#methods) - - [engine_newPayloadV3](#engine_newpayloadv3) + - [engine_newInclusionListV1](#engine_newinclusionlistv1) - [Request](#request) - [Response](#response) - [Specification](#specification) - - [engine_forkchoiceUpdatedV3](#engine_forkchoiceupdatedv3) + - [engine_newPayloadV4](#engine_newpayloadv4) - [Request](#request-1) - [Response](#response-1) - [Specification](#specification-1) - - [engine_getPayloadV3](#engine_getpayloadv3) + - [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) - [Deprecate `engine_exchangeTransitionConfigurationV1`](#deprecate-engine_exchangetransitionconfigurationv1) + - [Update the methods of previous forks](#update-the-methods-of-previous-forks) @@ -97,7 +103,7 @@ This structure contains the result of processing an inclusion list. The fields a * params: 1. `inclusion_list`: [`InclusionListV1`](#InclusionListV1). -#### Respose +#### Response - result: [`InclusionListStatusV1`](#InclusionListStatusV1). - error: code and message set in case an exception happens while processing the payload. diff --git a/specs/_features/epbs/validator.md b/specs/_features/epbs/validator.md index dc206820af..d1a5535be3 100644 --- a/specs/_features/epbs/validator.md +++ b/specs/_features/epbs/validator.md @@ -6,31 +6,19 @@ This document represents the changes and additions to the Honest validator guide **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* -- [ePBS -- Honest Validator](#epbs----honest-validator) - - [Introduction](#introduction) - - [Prerequisites](#prerequisites) - - [Protocols](#protocols) - - [`ExecutionEngine`](#executionengine) - - [`get_execution_inclusion_list`](#get_execution_inclusion_list) - - [Beacon chain responsibilities](#beacon-chain-responsibilities) - - [Validator assignment](#validator-assignment) - - [Lookahead](#lookahead) - - [Inclusion list proposal](#inclusion-list-proposal) - - [Constructing the inclusion list](#constructing-the-inclusion-list) - - [Broadcast inclusion list](#broadcast-inclusion-list) - - [Block proposal](#block-proposal) - - [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) - - [Aggregation selection](#aggregation-selection) - - [Payload timeliness attestation](#payload-timeliness-attestation) - - [Constructing payload attestation](#constructing-payload-attestation) - - [Prepare payload attestation message](#prepare-payload-attestation-message) - - [Broadcast payload attestation](#broadcast-payload-attestation) - - [Attesting](#attesting) - - [Attestation aggregation](#attestation-aggregation) - - [Design Rationale](#design-rationale) - - [What is the honest behavior to build on top of a skip slot for inclusion list?](#what-is-the-honest-behavior-to-build-on-top-of-a-skip-slot-for-inclusion-list) - - [Why skip the attestation if you are assigned to PTC?](#why-skip-the-attestation-if-you-are-assigned-to-ptc) +- [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) From 29a1c4d95542ff899cc987e14da0d3e9a5e6eb4a Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 5 Apr 2024 15:42:04 -0300 Subject: [PATCH 069/112] move hash to the inclusion list envelope --- specs/_features/epbs/beacon-chain.md | 4 ++-- specs/_features/epbs/validator.md | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 71e6381d9c..8d07ab317a 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -198,7 +198,6 @@ class InclusionListSummary(Container) ```python class SignedInclusionListSummary(Container): message: InclusionListSummary - parent_block_hash: Hash32 signature: BLSSignature ``` @@ -207,6 +206,7 @@ class SignedInclusionListSummary(Container): ```python class InclusionList(Container) signed_summary: SignedInclusionListSummary + parent_block_hash: Hash32 transactions: List[Transaction, MAX_TRANSACTIONS_PER_INCLUSION_LIST] ``` @@ -262,7 +262,7 @@ class ExecutionPayload(Container): withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] blob_gas_used: uint64 excess_blob_gas: uint64 - inclusion_list_summary: InclusionListSummary # [New in ePBS] + inclusion_list_summary: List[ExecutionAddress, MAX_TRANSACTIONS_PER_INCLUSION_LIST]# [New in ePBS] ``` #### `ExecutionPayloadHeader` diff --git a/specs/_features/epbs/validator.md b/specs/_features/epbs/validator.md index d1a5535be3..ed37d8aaae 100644 --- a/specs/_features/epbs/validator.md +++ b/specs/_features/epbs/validator.md @@ -120,11 +120,11 @@ def get_inclusion_list_summary_signature(state: BeaconState, inclusion_list_summ ``` ```python -def build_inclusion_list(state: BeaconState, inclusion_list_response: GetInclusionListResponse, block_slot: Slot, index: ValidatorIndex, privkey: int) -> InclusionList: - summary = InclusionListSummary(proposer_index=index, summary=inclusion_list_response.summary) +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, slot=block_slot, transactions=inclusion_list_response.transactions) + 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` From 2bde4399d960334100ca2ec0ceb67689f93952f6 Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 5 Apr 2024 20:24:58 -0300 Subject: [PATCH 070/112] use min instead of max for get_ptc --- specs/_features/epbs/beacon-chain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 8d07ab317a..a4ddaba451 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -395,7 +395,7 @@ 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(max(get_committee_count_per_slot(state, epoch), PTC_SIZE)) + 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 = [] From 5759e3465441f663f7798d0fabf0aa95bcadcc3d Mon Sep 17 00:00:00 2001 From: Potuz Date: Sun, 7 Apr 2024 10:51:58 -0300 Subject: [PATCH 071/112] add parent_block_root to the execution bid --- specs/_features/epbs/beacon-chain.md | 2 ++ specs/_features/epbs/builder.md | 13 +++++++------ specs/_features/epbs/p2p-interface.md | 1 + 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index a4ddaba451..13d7cf6d44 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -272,6 +272,7 @@ class ExecutionPayload(Container): ```python class ExecutionPayloadHeader(Container): parent_block_hash: Hash32 + parent_block_root: Root block_hash: Hash32 builder_index: ValidatorIndex slot: Slot @@ -519,6 +520,7 @@ def process_execution_payload_header(state: BeaconState, block: BeaconBlock) -> 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) diff --git a/specs/_features/epbs/builder.md b/specs/_features/epbs/builder.md index 751002d20b..5c480f8786 100644 --- a/specs/_features/epbs/builder.md +++ b/specs/_features/epbs/builder.md @@ -30,12 +30,13 @@ Builders can broadcast a payload bid for the current or the next slot's proposer 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. Construct an execution payload. This can be performed with an external execution engine with a call to `engine_getPayloadV4`. -3. Set `header.block_hash` to be the block hash of the constructed payload, that is `payload.block_hash` -4. Set `header.builder_index` to be the validator index of the builder performing these actions. -5. 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. -6. 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. -7. Set `header.kzg_commitments_root` to be the `hash_tree_root` of the `blobsbundle.commitments` field returned by `engine_getPayloadV4`. +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 diff --git a/specs/_features/epbs/p2p-interface.md b/specs/_features/epbs/p2p-interface.md index 3286235e22..6006a59f6a 100644 --- a/specs/_features/epbs/p2p-interface.md +++ b/specs/_features/epbs/p2p-interface.md @@ -145,6 +145,7 @@ 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. From de20380bafd68a4db61c1fd0a6dba3cf3ef6434d Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 8 Apr 2024 06:57:02 -0300 Subject: [PATCH 072/112] beacon block p2p changes --- specs/_features/epbs/p2p-interface.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/specs/_features/epbs/p2p-interface.md b/specs/_features/epbs/p2p-interface.md index 6006a59f6a..d3a891c81c 100644 --- a/specs/_features/epbs/p2p-interface.md +++ b/specs/_features/epbs/p2p-interface.md @@ -105,9 +105,26 @@ ePBS introduces new global topics for execution header, execution payload, paylo ###### `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. +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` @@ -145,7 +162,7 @@ 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. +- _[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. From c4aa0cd3e988dfc096f178f300b2f02f784ca131 Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 8 Apr 2024 08:14:35 -0300 Subject: [PATCH 073/112] on_block and keep unsigned header in state --- specs/_features/epbs/beacon-chain.md | 8 ++++---- specs/_features/epbs/fork-choice.md | 28 ++++++++++++---------------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 13d7cf6d44..b53bea84c0 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -333,7 +333,7 @@ class BeaconState(Container): latest_inclusion_list_slot: Slot # [New in ePBS] latest_block_hash: Hash32 # [New in ePBS] latest_full_slot: Slot # [New in ePBS] - signed_execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] + execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] last_withdrawals_root: Root # [New in ePBS] ``` @@ -383,7 +383,7 @@ This function returns true if the last committed payload header was fulfilled wi ```python def is_parent_block_full(state: BeaconState) -> bool: - return state.signed_execution_payload_header.message.block_hash == state.latest_block_hash + return state.execution_payload_header.block_hash == state.latest_block_hash ``` ### Beacon State accessors @@ -531,7 +531,7 @@ def process_execution_payload_header(state: BeaconState, block: BeaconBlock) -> state.latest_inclusion_list_proposer = block.proposer_index state.latest_inclusion_list_slot = block.slot # Cache the signed execution payload header - state.signed_execution_payload_header = signed_header + state.execution_payload_header = header ``` @@ -702,7 +702,7 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti # 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.signed_execution_payload_header.message + 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 diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index ee4cacc893..8ae061f38e 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -88,7 +88,8 @@ class Store(object): 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] - ptc_vote: Dict[Root, Vector[bool, PTC_SIZE]] = 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] ``` ### `verify_inclusion_list` @@ -340,9 +341,7 @@ def notify_new_inclusion_list(self: ExecutionEngine, ### `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. There is also the addition of the inclusion list availability check. - -In addition we delay the checking of blob availability until the processing of the execution payload. +*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: @@ -355,15 +354,16 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: # Check if this blocks builds on empty or full parent block parent_block = store.blocks[block.parent_root] - parent_signed_payload_header_envelope = parent_block.body.signed_execution_payload_header_envelope - parent_payload_hash = parent_signed_payload_header_envelope.message.header.block_hash - current_signed_payload_header_envelope = block.body.signed_execution_payload_header_envelope - current_payload_parent_hash = current_signed_payload_header_envelope.message.header.parent_hash + header = block.body.signed_execution_payload_header.message + parent_header = parent_block.body.signed_execution_payload_header.message + parent_payload_hash = parent_header.block_hash + current_payload_parent_hash = header.parent_block_hash # Make a copy of the state to avoid mutability issues if current_payload_parent_hash == parent_payload_hash: assert block.parent_root in store.execution_payload_states state = copy(store.execution_payload_states[block.parent_root]) else: + assert current_payload_parent_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. @@ -381,12 +381,6 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: ) assert store.finalized_checkpoint.root == finalized_checkpoint_block - # Check if there is a valid inclusion list. - # This check is performed only if the block's slot is within the visibility window - # If not, this block MAY be queued and subsequently considered when a valid inclusion list becomes available - if block.slot + MIN_SLOTS_FOR_INCLUSION_LISTS_REQUESTS >= current_slot: - assert is_inclusion_list_available(state, block) - # Check the block is valid and compute the post-state block_root = hash_tree_root(block) state_transition(state, signed_block, True) @@ -396,11 +390,13 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: # 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] = [False]*PTC_SIZE + 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 current_payload_parent_hash == parent_header.parent_block_hash: + 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 From 99132a4d3d80f5bf2a67b77d7f4e0412c46c7358 Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 8 Apr 2024 08:15:34 -0300 Subject: [PATCH 074/112] changed type in state too --- specs/_features/epbs/beacon-chain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index b53bea84c0..63fd550ac2 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -333,7 +333,7 @@ class BeaconState(Container): latest_inclusion_list_slot: Slot # [New in ePBS] latest_block_hash: Hash32 # [New in ePBS] latest_full_slot: Slot # [New in ePBS] - execution_payload_header: SignedExecutionPayloadHeader # [New in ePBS] + execution_payload_header: ExecutionPayloadHeader # [New in ePBS] last_withdrawals_root: Root # [New in ePBS] ``` From c9e1834d878a865cf4e027b93e0910014a51ad7b Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 8 Apr 2024 08:41:38 -0300 Subject: [PATCH 075/112] penalize twice the proposer for including bad ptc --- specs/_features/epbs/beacon-chain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 63fd550ac2..8c1dd9b836 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -639,7 +639,7 @@ def process_payload_attestation(state: BeaconState, payload_attestation: Payload epoch_participation[index] = remove_flag(flag_index) proposer_penalty_numerator += get_base_reward(state, index) * weight # Penalize the proposer - proposer_penalty = Gwei(proposer_penalty_numerator // proposer_reward_denominator) + proposer_penalty = Gwei(2*proposer_penalty_numerator // proposer_reward_denominator) decrease_balance(state, proposer_index, proposer_penalty) return From f785453252cafcd9e8343e81990093b9f100a65d Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 8 Apr 2024 10:04:30 -0300 Subject: [PATCH 076/112] inclusion list forkchoice handler --- specs/_features/epbs/beacon-chain.md | 2 +- specs/_features/epbs/engine.md | 26 ----------- specs/_features/epbs/fork-choice.md | 69 +++++++++++++++++++++------- 3 files changed, 53 insertions(+), 44 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 8c1dd9b836..6f2d2329df 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -379,7 +379,7 @@ def is_valid_indexed_payload_attestation(state: BeaconState, indexed_payload_att #### `is_parent_block_full` -This function returns true if the last committed payload header was fulfilled with a full payload, this can only happen when the parent block was full, that is both beacon block and payload were present. +This function returns true if the last committed payload header was fulfilled with a full payload, this can only happen when the parent block was full, that is 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: diff --git a/specs/_features/epbs/engine.md b/specs/_features/epbs/engine.md index ee39e9ae48..77acc5a8c5 100644 --- a/specs/_features/epbs/engine.md +++ b/specs/_features/epbs/engine.md @@ -29,8 +29,6 @@ Engine API changes introduced in the ePBS fork - [Request](#request-3) - [Response](#response-3) - [Specification](#specification-3) - - [Deprecate `engine_exchangeTransitionConfigurationV1`](#deprecate-engine_exchangetransitionconfigurationv1) - - [Update the methods of previous forks](#update-the-methods-of-previous-forks) @@ -203,27 +201,3 @@ Refer to the specification for [`engine_getPayloadV2`](./shanghai.md#engine_getp 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`. - -### Deprecate `engine_exchangeTransitionConfigurationV1` - -This document introduces deprecation of [`engine_exchangeTransitionConfigurationV1`](./paris.md#engine_exchangetransitionconfigurationv1). The deprecation is specified as follows: - -1. Consensus layer clients **MUST NOT** call this method. - -2. Execution layer clients **MUST NOT** surface an error message to the user if this method is not called. - -3. Consensus and execution layer clients **MAY** remove support of this method after Cancun. If no longer supported, this method **MUST** be removed from the [`engine_exchangeCapabilities`](./common.md#engine_exchangecapabilities) request or response list depending on whether it is consensus or execution layer client. - -### Update the methods of previous forks - -This document defines how Cancun payload should be handled by the [`Shanghai API`](./shanghai.md). - -For the following methods: - -- [`engine_forkchoiceUpdatedV2`](./shanghai.md#engine_forkchoiceupdatedv2) -- [`engine_newPayloadV2`](./shanghai.md#engine_newpayloadV2) -- [`engine_getPayloadV2`](./shanghai.md#engine_getpayloadv2) - -a validation **MUST** be added: - -1. Client software **MUST** return `-38005: Unsupported fork` error if the `timestamp` of payload or payloadAttributes greater or equal to the Cancun activation timestamp. diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 8ae061f38e..20de3936b5 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -12,7 +12,9 @@ - [Modified `update_latest_messages`](#modified-update_latest_messages) - [Modified `Store`](#modified-store) - [`verify_inclusion_list`](#verify_inclusion_list) - - [`is_inclusion_list_available`](#is_inclusion_list_available) + - [`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) - [Modified `get_ancestor`](#modified-get_ancestor) @@ -114,40 +116,73 @@ def verify_inclusion_list(state: BeaconState, block: BeaconBlock, inclusion_list assert len(summary) <= MAX_TRANSACTIONS_PER_INCLUSION_LIST assert len(inclusion_list.transactions) == len(summary) - # TODO: These checks will also be performed by the EL surely so we can probably remove them from here. - # Check that the total gas limit is bounded - total_gas_limit = sum( entry.gas_limit for entry in summary ) - assert total_gas_limit <= MAX_GAS_PER_INCLUSION_LIST - # Check that the inclusion list is valid return execution_engine.notify_new_inclusion_list(NewInclusionListRequest( inclusion_list=inclusion_list.transactions, - summary=inclusion_list.summary.message.summary, + summary=summary, parent_block_hash = state.latest_execution_payload_header.block_hash)) ``` -### `is_inclusion_list_available` +### `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.slot == summary.slot and 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 is_inclusion_list_available(state: BeaconState, block: BeaconBlock) -> bool: +def on_inclusion_list(store: Store, inclusion_list: InclusionList) -> None: """ - Returns whether one inclusion list for the corresponding block was seen in full and has been validated. - There is one exception if the parent consensus block did not contain an exceution payload, in which case - We return true early + Validates an incoming inclusion list and records the result in the corresponding forkchoice node. `retrieve_inclusion_list` is implementation and context dependent - It returns one inclusion list that was broadcasted during the given slot by the given proposer. + It returns one inclusion list that was broadcast during the given slot by the given proposer. Note: the p2p network does not guarantee sidecar retrieval outside of `MIN_SLOTS_FOR_INCLUSION_LISTS_REQUESTS` """ + # 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 - if !is_parent_block_full(state): - return True + 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 - inclusion_list = retrieve_inclusion_list(block.slot, block.proposer_index) - return verify_inclusion_list(state, block, inclusion_list, EXECUTION_ENGINE) + assert verify_inclusion_list(state, block, inclusion_list, EXECUTION_ENGINE) + store.inclusion_list_available[root]=True ``` ### `notify_ptc_messages` From 075837e507c340916a88cce11701920fbd5e10be Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 8 Apr 2024 11:56:14 -0300 Subject: [PATCH 077/112] ptc_message forkchoice handler --- specs/_features/epbs/beacon-chain.md | 1 - specs/_features/epbs/fork-choice.md | 8 +++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 6f2d2329df..d26a40ffc4 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -690,7 +690,6 @@ def process_execution_payload(state: BeaconState, signed_envelope: SignedExecuti # 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 - assert payload. # Verify inclusion list summary signature signed_summary = SignedInclusionListSummary( message=InclusionListSummary( diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 20de3936b5..3a3e3126c2 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -488,21 +488,19 @@ def on_payload_attestation_message(store: Store, # 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) - # Check that the attester is from the current ptc - assert ptc_message.validator_index in ptc # 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 - # TODO: Do we want to slash ptc members that equivocate? - # we are updating here the message and so the last vote will be the one that counts. ptc_index = ptc.index(ptc_message.validator_index) ptc_vote = store.ptc_vote[data.beacon_block_root] - ptc_vote[ptc_index] = data.present + ptc_vote[ptc_index] = data.payload_status ``` From 0659a919d0e45859d3f329a8e7456efcdcea2e1a Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 12 Apr 2024 12:58:59 -0300 Subject: [PATCH 078/112] only check payload hash if not withheld --- specs/_features/epbs/p2p-interface.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specs/_features/epbs/p2p-interface.md b/specs/_features/epbs/p2p-interface.md index d3a891c81c..44d47826a3 100644 --- a/specs/_features/epbs/p2p-interface.md +++ b/specs/_features/epbs/p2p-interface.md @@ -139,7 +139,8 @@ 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` -- _[REJECT]_ `payload.block_hash == header.block_hash` +- 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` From 4d799b289c0920837c047a21599e989fb0ba917c Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 15 Apr 2024 18:20:44 -0300 Subject: [PATCH 079/112] forkchoice no payload boosts --- specs/_features/epbs/fork-choice.md | 45 ++++++++++++++--------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 3a3e3126c2..4d8f5d8e4c 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -158,11 +158,6 @@ The function `on_inclusion_list` is called every time an `InclusionList` is seen def on_inclusion_list(store: Store, inclusion_list: InclusionList) -> None: """ Validates an incoming inclusion list and records the result in the corresponding forkchoice node. - - `retrieve_inclusion_list` is implementation and context dependent - It returns one inclusion list that was broadcast during the given slot by the given proposer. - Note: the p2p network does not guarantee sidecar retrieval outside of - `MIN_SLOTS_FOR_INCLUSION_LISTS_REQUESTS` """ # Require we have one block compatible with the inclusion list block = block_for_inclusion_list(store, inclusion_list) @@ -192,6 +187,8 @@ def notify_ptc_messages(store: Store, state: BeaconState, payload_attestations: """ Extracts a list of ``PayloadAttestationMessage`` from ``payload_attestations`` and updates the store with them """ + 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: @@ -209,7 +206,7 @@ def is_payload_present(store: Store, beacon_block_root: Root) -> bool: """ # The beacon block root must be known assert beacon_block_root in store.ptc_vote - return store.ptc_vote[beacon_block_root].count(True) > PAYLOAD_TIMELY_THRESHOLD + return store.ptc_vote[beacon_block_root].count(PAYLOAD_PRESENT) > PAYLOAD_TIMELY_THRESHOLD ``` ### Modified `get_ancestor` @@ -225,11 +222,13 @@ def get_ancestor(store: Store, root: Root, slot: Slot) -> tuple[Root, bool]: block = store.blocks[root] if block.slot == slot: return [root, store.is_payload_present(root)] + + assert block.slot > slot parent = store.blocks[block.parent_root] if parent.slot > slot: return get_ancestor(store, block.parent_root, slot) - if block.body.signed_execution_payload_header_envelope.message.parent_hash == - parent.body.signed_execution_payload_header_envelope.message.block_hash: + if block.body.signed_execution_payload_header.message.parent_block_hash == + parent.body.signed_execution_payload_header.message.block_hash: return (block.parent_root, True) return (block.parent_root, False) ``` @@ -258,6 +257,7 @@ def is_supporting_vote(store: Store, root: Root, slot: Slot, is_payload_present: """ if 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 slot <= message.slot message_block = store.blocks[message.root] if slot > message_block.slot: @@ -268,7 +268,7 @@ def is_supporting_vote(store: Store, root: Root, slot: Slot, is_payload_present: ### 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. +**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, root: Root, slot: Slot, is_payload_present: bool) -> Gwei: @@ -297,12 +297,12 @@ def get_weight(store: Store, root: Root, slot: Slot, is_payload_present: bool) - return attestation_score + proposer_score ``` -### Modified `get_head` +### New `get_head_no_il` -**Note:** `get_head` is modified 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. +**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(store: Store) -> tuple[Root, bool]: +def get_head_no_il(store: Store) -> tuple[Root, bool]: # Get filtered block tree that only includes viable branches blocks = get_filtered_block_tree(store) # Execute the LMD-GHOST fork choice @@ -323,7 +323,6 @@ def get_head(store: Store) -> tuple[Root, bool]: # Ties broken by favoring full blocks # Ties broken then by favoring higher slot numbers # Ties then broken by favoring block with lexicographically higher root - # TODO: Can (root, full), (root, empty) have the same weight? child_root = max(children, key=lambda (root, slot, present): (get_weight(store, root, slot, present), present, slot, root)) if child_root == head_root: return (head_root, head_full) @@ -331,20 +330,18 @@ def get_head(store: Store) -> tuple[Root, bool]: head_full = is_payload_present(store, head_root) ``` -### New `get_block_hash` +### 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_blockhash(store: Store, root: Root) -> Hash32: - """ - returns the blockHash of the latest execution payload in the chain containing the - beacon block with root ``root`` - """ - # The block is known - if is_payload_present(store, root): - return hash(store.execution_payload_states[root].latest_block_header) - return hash(store.block__states[root].latest_block_header) +def get_head(store: Store) -> tuple[Root, bool]: + head_root, _ = get_head_no_il(store) + while not store.inclusion_list_available(head_root): + head_block = store.blocks[head_root] + head_root = head_block.parent_root + return head_root, store.is_payload_present(head_root) ``` - + ## Engine APIs ### New `NewInclusionListRequest` From 7b6fa0f40dfb44c85cc2c787862b93274b499d3d Mon Sep 17 00:00:00 2001 From: terence tsao Date: Mon, 15 Apr 2024 13:40:41 -0700 Subject: [PATCH 080/112] Fix ePBS beacon_chain.md typos --- specs/_features/epbs/beacon-chain.md | 38 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index d26a40ffc4..04788e029a 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -397,12 +397,12 @@ def get_ptc(state: BeaconState, slot: Slot) -> Vector[ValidatorIndex, PTC_SIZE]: """ 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 + members_per_committee = PTC_SIZE // committees_per_slot validator_indices = [] - for idx in range(committees_per_slot) + for idx in range(committees_per_slot): beacon_committee = get_beacon_committee(state, slot, idx) - validator_indices += beacon_committee[:members_per_commitee] + validator_indices += beacon_committee[:members_per_committee] return validator_indices ``` @@ -451,7 +451,7 @@ The post-state corresponding to a pre-state `state` and a signed execution paylo ```python def process_block(state: BeaconState, block: BeaconBlock) -> None: - process_block_header(state, block) # + process_block_header(state, block) removed process_withdrawals(state) [Modified in ePBS] process_execution_payload_header(state, block) # [Modified in ePBS, removed process_execution_payload] process_randao(state, block.body) @@ -466,7 +466,7 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None: ```python def process_withdrawals(state: BeaconState) -> None: ## return early if the parent block was empty - if !is_parent_block_full(state): + if not is_parent_block_full(state): return withdrawals = get_expected_withdrawals(state) @@ -517,10 +517,10 @@ def process_execution_payload_header(state: BeaconState, block: BeaconBlock) -> assert state.balances[builder_index] >= amount # Verify that the bid is for the current slot - assert header.slot = block.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 + 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) @@ -587,7 +587,7 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None: 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 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) @@ -603,7 +603,7 @@ def process_attestation(state: BeaconState, attestation: Attestation) -> None: ```python def remove_flag(flags: ParticipationFlags, flag_index: int) -> ParticipationFlags: - flag = PartitipationFlags(2**flag_index) + flag = ParticipationFlags(2**flag_index) return flags & ~flag ``` @@ -627,16 +627,16 @@ def process_payload_attestation(state: BeaconState, payload_attestation: Payload # Return early if the attestation is for the wrong payload status payload_was_present = data.slot == state.latest_full_slot - voted_preset = data.payload_status == PAYLOAD_PRESENT + 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_atterstation.attesting_indices: + 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(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) @@ -649,7 +649,7 @@ def process_payload_attestation(state: BeaconState, payload_attestation: Payload 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 += base_reward * weight + proposer_reward_numerator += get_base_reward(state, index) * weight # Reward proposer proposer_reward = Gwei(proposer_reward_numerator // proposer_reward_denominator) @@ -682,20 +682,20 @@ def verify_inclusion_list_summary_signature(state: BeaconState, signed_summary: def process_execution_payload(state: BeaconState, signed_envelope: SignedExecutionPayloadEnvelope, execution_engine: ExecutionEngine, verify = True) -> None: # Verify signature if verify: - assert verify_execution_envelope_signature(state, signed_envelope) + 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 + 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) + 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 From 3411f45a66222d27321ef5f8bdf6b9d21c0dd3fb Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 16 Apr 2024 13:23:03 -0300 Subject: [PATCH 081/112] Add payload boosts --- specs/_features/epbs/fork-choice.md | 79 +++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 9 deletions(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 4d8f5d8e4c..fbf24e9439 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -44,7 +44,9 @@ This is the modification of the fork choice accompanying the ePBS upgrade. | 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` | ## Helpers ### Modified `LatestMessage` @@ -92,6 +94,8 @@ class Store(object): 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_reveal_boost_root: Root # [New in ePBS] ``` ### `verify_inclusion_list` @@ -266,6 +270,20 @@ def is_supporting_vote(store: Store, root: Root, slot: Slot, is_payload_present: return (root == ancestor_root) and (is_payload_preset == is_ancestor_full) ``` +### New `compute_boost` + +```python +def compute_boost(store: Store, state: State, root: Root, boost_root: Root, is_payload_present: bool, boost_value: uint64) -> Gwei: + boost = Gwei(0) + if boost_root == Root(): + return boost + (ancestor_root, is_ancestor_full) = get_ancestor(store, boost_root, store.blocks[root].slot) + if (ancestor_root == root) and (is_ancestor_full == is_payload_present): + committee_weight = get_total_active_balance(state) // SLOTS_PER_EPOCH + boost = (committee_weight * boost_value) // 100 + return boost +``` + ### 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. @@ -287,14 +305,13 @@ def get_weight(store: Store, root: Root, slot: Slot, is_payload_present: bool) - # Return only attestation score if ``proposer_boost_root`` is not set return attestation_score - # Calculate proposer score if ``proposer_boost_root`` is set - proposer_score = Gwei(0) - # Boost is applied if ``root`` is an ancestor of ``proposer_boost_root`` - (ancestor_root, is_ancestor_full) = get_ancestor(store, store.proposer_boost_root, store.blocks[root].slot) - if (ancestor_root == root) and (is_ancestor_full == is_payload_present): - committee_weight = get_total_active_balance(state) // SLOTS_PER_EPOCH - proposer_score = (committee_weight * PROPOSER_SCORE_BOOST) // 100 - return attestation_score + proposer_score + + proposer_score = compute_boost(store, state, root, store.proposer_boost_root, is_payload_present, PROPOSER_SCORE_BOOST) + + builder_reveal_score = compute_boost(store, state, root, store.payload_reveal_boost_root, is_payload_present, PAYLOAD_REVEAL_BOOST) + builder_withhold_score = compute_boost(store, state, root, store.payload_withhold_boost_root, is_payload_present, PAYLOAD_WITHHOLD_BOOST) + + return attestation_score + proposer_score + builder_reveal_score + builder_withhold_score ``` ### New `get_head_no_il` @@ -469,6 +486,40 @@ def on_excecution_payload(store: Store, signed_envelope: SignedExecutionPayloadE 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_withheld_boost_root = Root() + 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 @@ -500,4 +551,14 @@ def on_payload_attestation_message(store: Store, ptc_index = ptc.index(ptc_message.validator_index) ptc_vote = store.ptc_vote[data.beacon_block_root] ptc_vote[ptc_index] = data.payload_status + + if data.slot != get_current_slot(store): + 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_withheld_boost_root = block.parent_root ``` From b33c222e89532a31575e5dce5f743dab04ff9616 Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 16 Apr 2024 13:27:32 -0300 Subject: [PATCH 082/112] doctoc --- specs/_features/epbs/fork-choice.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index fbf24e9439..883e0d68ce 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -7,7 +7,6 @@ - [Introduction](#introduction) - [Constant](#constant) -- [Helpers](#helpers) - [Modified `LatestMessage`](#modified-latestmessage) - [Modified `update_latest_messages`](#modified-update_latest_messages) - [Modified `Store`](#modified-store) @@ -20,9 +19,10 @@ - [Modified `get_ancestor`](#modified-get_ancestor) - [Modified `get_checkpoint_block`](#modified-get_checkpoint_block) - [`is_supporting_vote`](#is_supporting_vote) + - [New `compute_boost`](#new-compute_boost) - [Modified `get_weight`](#modified-get_weight) + - [New `get_head_no_il`](#new-get_head_no_il) - [Modified `get_head`](#modified-get_head) - - [New `get_block_hash`](#new-get_block_hash) - [Engine APIs](#engine-apis) - [New `NewInclusionListRequest`](#new-newinclusionlistrequest) - [New `notify_new_inclusion_list`](#new-notify_new_inclusion_list) @@ -30,6 +30,8 @@ - [`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) From 680bcdba27e2e6a117aef2ef93a498d23c309eb9 Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 16 Apr 2024 14:44:04 -0300 Subject: [PATCH 083/112] fix typo withheld -> withhold --- specs/_features/epbs/fork-choice.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 883e0d68ce..5716f945a5 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -514,7 +514,7 @@ def on_tick_per_slot(store: Store, time: uint64) -> None: else: # reset the payload boost if this is the attestation time if seconds_into_slot(store) >= SECONDS_PER_SLOT // INTERVALS_PER_SLOT: - store.payload_withheld_boost_root = Root() + store.payload_withhold_boost_root = Root() store.payload_reveal_boost_root = Root() # If a new epoch, pull-up justification and finalization from previous epoch @@ -562,5 +562,5 @@ def on_payload_attestation_message(store: Store, 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_withheld_boost_root = block.parent_root + store.payload_withhold_boost_root = block.parent_root ``` From 25dde5eed5aa3456d48259f18abb6b3029d25457 Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 16 Apr 2024 14:55:45 -0300 Subject: [PATCH 084/112] apply boost for attestations from blocks as well --- specs/_features/epbs/fork-choice.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 5716f945a5..1b742c324b 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -554,7 +554,11 @@ def on_payload_attestation_message(store: Store, ptc_vote = store.ptc_vote[data.beacon_block_root] ptc_vote[ptc_index] = data.payload_status - if data.slot != get_current_slot(store): + # 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 time_into_slot >= SECONDS_PER_SLOT // INTERVALS_PER_SLOT: return # Update the payload boosts if threshold has been achieved From 9fa6e14f06c8e5464d452e8b1dd1f14b215544e8 Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 16 Apr 2024 17:21:32 -0300 Subject: [PATCH 085/112] typo --- specs/_features/epbs/fork-choice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 1b742c324b..ab854394c7 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -266,7 +266,7 @@ def is_supporting_vote(store: Store, root: Root, slot: Slot, is_payload_present: # as long as the attestation happened after the requested slot. return slot <= message.slot message_block = store.blocks[message.root] - if slot > message_block.slot: + if slot >= message_block.slot: return False (ancestor_root, is_ancestor_full) = get_ancestor(store, message.root, slot) return (root == ancestor_root) and (is_payload_preset == is_ancestor_full) From 960e92dadb8644cd7dab436f12945a2bb6784cb3 Mon Sep 17 00:00:00 2001 From: terence tsao Date: Tue, 16 Apr 2024 10:47:40 -0700 Subject: [PATCH 086/112] Fix some forkchoice typos --- specs/_features/epbs/fork-choice.md | 32 ++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index ab854394c7..fb3db52d96 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -106,7 +106,7 @@ class Store(object): ```python def verify_inclusion_list(state: BeaconState, block: BeaconBlock, inclusion_list: InclusionList, execution_engine: ExecutionEngine) -> bool: """ - returns true if the inclusion list is valid. + Returns true if the inclusion list is valid. """ # Check that the inclusion list corresponds to the block proposer signed_summary = inclusion_list.summary @@ -126,7 +126,7 @@ def verify_inclusion_list(state: BeaconState, block: BeaconBlock, inclusion_list return execution_engine.notify_new_inclusion_list(NewInclusionListRequest( inclusion_list=inclusion_list.transactions, summary=summary, - parent_block_hash = state.latest_execution_payload_header.block_hash)) + parent_block_hash = state.execution_payload_header.block_hash)) ``` ### `blocks_for_slot` @@ -141,7 +141,7 @@ def blocks_for_slot(store: Store, slot: Slot) -> Set[BeaconBlock]: ### `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 +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]: @@ -150,7 +150,7 @@ def block_for_inclusion_list(store: Store, inclusion_list: InclusionList) -> Opt blocks = blocks_for_slot(store, summary.slot) for block in blocks: - if block.slot == summary.slot and block.proposer_index == summary.proposer_index and block.signed_execution_payload_header.message.parent_block_hash == parent_hash: + if block.proposer_index == summary.proposer_index and block.signed_execution_payload_header.message.parent_block_hash == parent_hash: return block return None ``` @@ -177,7 +177,7 @@ def on_inclusion_list(store: Store, inclusion_list: InclusionList) -> None: # 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 + if header.parent_block_hash != parent_header.block_hash: assert header.parent_block_hash == parent_header.parent_block_hash return @@ -192,6 +192,7 @@ def on_inclusion_list(store: Store, inclusion_list: InclusionList) -> None: 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 @@ -199,7 +200,7 @@ def notify_ptc_messages(store: Store, state: BeaconState, 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) + data=payload_attestation.data, signature= BLSSignature(), is_from_block=true)) ``` ### `is_payload_present` @@ -207,7 +208,7 @@ def notify_ptc_messages(store: Store, state: BeaconState, payload_attestations: ```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 + 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 @@ -221,20 +222,19 @@ def is_payload_present(store: Store, beacon_block_root: Root) -> bool: ```python def get_ancestor(store: Store, root: Root, slot: Slot) -> tuple[Root, bool]: """ - returns the beacon block root of the ancestor of the beacon block with ``root`` at``slot`` and it also + Returns the beacon block root of the ancestor of the beacon block with ``root`` at ``slot`` and it also returns ``true`` if it based on a full block and ``false`` otherwise. If the beacon block with ``root`` is already at ``slot`` it returns it's PTC status. """ block = store.blocks[root] if block.slot == slot: - return [root, store.is_payload_present(root)] + return (root, store.is_payload_present(root)) assert block.slot > slot parent = store.blocks[block.parent_root] if parent.slot > slot: return get_ancestor(store, block.parent_root, slot) - if block.body.signed_execution_payload_header.message.parent_block_hash == - parent.body.signed_execution_payload_header.message.block_hash: + if block.body.signed_execution_payload_header.message.parent_block_hash == parent.body.signed_execution_payload_header.message.block_hash: return (block.parent_root, True) return (block.parent_root, False) ``` @@ -258,7 +258,7 @@ def get_checkpoint_block(store: Store, root: Root, epoch: Epoch) -> Root: ```python def is_supporting_vote(store: Store, root: Root, slot: Slot, is_payload_present: bool, message: LatestMessage) -> bool: """ - returns whether a vote for ``message.root`` supports the chain containing the beacon block ``root`` with the + Returns whether a vote for ``message.root`` supports the chain containing the beacon block ``root`` with the payload contents indicated by ``is_payload_present`` as head during slot ``slot``. """ if root == message.root: @@ -269,7 +269,7 @@ def is_supporting_vote(store: Store, root: Root, slot: Slot, is_payload_present: if slot >= message_block.slot: return False (ancestor_root, is_ancestor_full) = get_ancestor(store, message.root, slot) - return (root == ancestor_root) and (is_payload_preset == is_ancestor_full) + return (root == ancestor_root) and (is_payload_present == is_ancestor_full) ``` ### New `compute_boost` @@ -466,7 +466,7 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: ### `on_execution_payload` ```python -def on_excecution_payload(store: Store, signed_envelope: SignedExecutionPayloadEnvelope) -> None: +def on_execution_payload(store: Store, signed_envelope: SignedExecutionPayloadEnvelope) -> None: """ Run ``on_execution_payload`` upon receiving a new execution payload. """ @@ -484,7 +484,7 @@ def on_excecution_payload(store: Store, signed_envelope: SignedExecutionPayloadE # Process the execution payload process_execution_payload(state, signed_envelope, EXECUTION_ENGINE) - #Add new state for this payload to the store + # Add new state for this payload to the store store.execution_payload_states[envelope.beacon_block_root] = state ``` @@ -512,7 +512,7 @@ def on_tick_per_slot(store: Store, time: uint64) -> None: if current_slot > previous_slot: store.proposer_boost_root = Root() else: - # reset the payload boost if this is the attestation time + # 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_reveal_boost_root = Root() From 018fa39644b37d6d05c172974868cff58463b2d7 Mon Sep 17 00:00:00 2001 From: terence Date: Tue, 16 Apr 2024 16:57:42 -0700 Subject: [PATCH 087/112] Update specs/_features/epbs/fork-choice.md Co-authored-by: Potuz --- specs/_features/epbs/fork-choice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index fb3db52d96..578bb84832 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -126,7 +126,7 @@ def verify_inclusion_list(state: BeaconState, block: BeaconBlock, inclusion_list return execution_engine.notify_new_inclusion_list(NewInclusionListRequest( inclusion_list=inclusion_list.transactions, summary=summary, - parent_block_hash = state.execution_payload_header.block_hash)) + parent_block_hash = state.latest_block_hash)) ``` ### `blocks_for_slot` From ac93d02ccd4b18ed4307e110dde4fe70a83f135a Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 16 Apr 2024 22:00:30 -0300 Subject: [PATCH 088/112] fix get_head to take all children with or without payload from justified --- specs/_features/epbs/fork-choice.md | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 578bb84832..e3f7fbcfa1 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -316,6 +316,16 @@ def get_weight(store: Store, root: Root, slot: Slot, is_payload_present: bool) - return attestation_score + proposer_score + builder_reveal_score + builder_withhold_score ``` +### New `ChildNode` +Auxiliary class to consider `(block, slot, bool)` LMD voting + +```python +class ChildNode(Container): + root: Root + slot: Slot + is_payload_present: bool +``` + ### 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. @@ -331,22 +341,22 @@ def get_head_no_il(store: Store) -> tuple[Root, bool]: head_full = is_payload_present(store, head_root) while True: children = [ - (root, block.slot, present) for (root, block) in blocks.items() + ChildNode(root=root, slot=block.slot, is_payload_present=present) for (root, block) in blocks.items() if block.parent_root == head_root for present in (True, False) ] if len(children) == 0: return (head_root, head_full) # if we have children we consider the current head advanced as a possible head - children += [(head_root, head_slot + 1, head_full)] + children += [ChildNode(root=head_root, slot=head_slot + 1, present) for present in (True, False)] # Sort by latest attesting balance with ties broken lexicographically # Ties broken by favoring full blocks # Ties broken then by favoring higher slot numbers # Ties then broken by favoring block with lexicographically higher root - child_root = max(children, key=lambda (root, slot, present): (get_weight(store, root, slot, present), present, slot, root)) - if child_root == head_root: - return (head_root, head_full) - head_root = child_root - head_full = is_payload_present(store, head_root) + best_child = max(children, key=lambda child: (get_weight(store, child.root, child.slot, child.is_payload_present), child.is_payload_present, child.slot, child.root)) + if best_child.root == head_root: + return (best_child.root, best_child.is_payload_present) + head_root = best_child.root + head_full = best_child.is_payload_present ``` ### Modified `get_head` From 727a0430ffedd6120c7d34306e0a075cdaeca0d7 Mon Sep 17 00:00:00 2001 From: Potuz Date: Tue, 16 Apr 2024 22:08:14 -0300 Subject: [PATCH 089/112] do not compute the same child twice --- specs/_features/epbs/fork-choice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index e3f7fbcfa1..7aba257455 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -347,7 +347,7 @@ def get_head_no_il(store: Store) -> tuple[Root, bool]: if len(children) == 0: return (head_root, head_full) # if we have children we consider the current head advanced as a possible head - children += [ChildNode(root=head_root, slot=head_slot + 1, present) for present in (True, False)] + children += [ChildNode(root=head_root, slot=head_slot + 1, head_full)] # Sort by latest attesting balance with ties broken lexicographically # Ties broken by favoring full blocks # Ties broken then by favoring higher slot numbers From 5f97041068a9326f96554c8b7898e126574cdcaa Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 17 Apr 2024 10:48:38 -0300 Subject: [PATCH 090/112] Revert "do not compute the same child twice" This reverts commit 727a0430ffedd6120c7d34306e0a075cdaeca0d7. --- specs/_features/epbs/fork-choice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 7aba257455..e3f7fbcfa1 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -347,7 +347,7 @@ def get_head_no_il(store: Store) -> tuple[Root, bool]: if len(children) == 0: return (head_root, head_full) # if we have children we consider the current head advanced as a possible head - children += [ChildNode(root=head_root, slot=head_slot + 1, head_full)] + children += [ChildNode(root=head_root, slot=head_slot + 1, present) for present in (True, False)] # Sort by latest attesting balance with ties broken lexicographically # Ties broken by favoring full blocks # Ties broken then by favoring higher slot numbers From 84280a7b9650547f2d9f310961610fc64321fa0d Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 17 Apr 2024 10:55:00 -0300 Subject: [PATCH 091/112] Reapply "do not compute the same child twice" This reverts commit 5f97041068a9326f96554c8b7898e126574cdcaa. --- specs/_features/epbs/fork-choice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index e3f7fbcfa1..7aba257455 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -347,7 +347,7 @@ def get_head_no_il(store: Store) -> tuple[Root, bool]: if len(children) == 0: return (head_root, head_full) # if we have children we consider the current head advanced as a possible head - children += [ChildNode(root=head_root, slot=head_slot + 1, present) for present in (True, False)] + children += [ChildNode(root=head_root, slot=head_slot + 1, head_full)] # Sort by latest attesting balance with ties broken lexicographically # Ties broken by favoring full blocks # Ties broken then by favoring higher slot numbers From b68d884c5752bb83c3209c2667ca2db3fb56dc86 Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 17 Apr 2024 11:35:47 -0300 Subject: [PATCH 092/112] only iterate over children with actual payloads --- specs/_features/epbs/fork-choice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 7aba257455..c91acbc8f3 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -342,7 +342,7 @@ def get_head_no_il(store: Store) -> tuple[Root, bool]: while True: children = [ ChildNode(root=root, slot=block.slot, is_payload_present=present) for (root, block) in blocks.items() - if block.parent_root == head_root for present in (True, False) + if block.parent_root == head_root for present in (True, False) if root in store.execution_payload_states or present == False ] if len(children) == 0: return (head_root, head_full) From 86745170158c3cb2eb60c92b3c495e8899d383a1 Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 17 Apr 2024 11:44:21 -0300 Subject: [PATCH 093/112] consider PTC vote untying max weight --- specs/_features/epbs/fork-choice.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index c91acbc8f3..4f0d0483b4 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -349,10 +349,11 @@ def get_head_no_il(store: Store) -> tuple[Root, bool]: # if we have children we consider the current head advanced as a possible head children += [ChildNode(root=head_root, slot=head_slot + 1, head_full)] # Sort by latest attesting balance with ties broken lexicographically - # Ties broken by favoring full blocks + # 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 - best_child = max(children, key=lambda child: (get_weight(store, child.root, child.slot, child.is_payload_present), child.is_payload_present, child.slot, child.root)) + best_child = max(children, key=lambda child: (get_weight(store, child.root, child.slot, child.is_payload_present), is_payload_present(child.root), child.is_payload_present, child.slot, child.root)) if best_child.root == head_root: return (best_child.root, best_child.is_payload_present) head_root = best_child.root From b2c64823c8de2f69bf4b47e0d225ec8eaa7e74bd Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 17 Apr 2024 12:47:17 -0300 Subject: [PATCH 094/112] Fix boost computation --- specs/_features/epbs/fork-choice.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 4f0d0483b4..38f274c035 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -275,15 +275,16 @@ def is_supporting_vote(store: Store, root: Root, slot: Slot, is_payload_present: ### New `compute_boost` ```python -def compute_boost(store: Store, state: State, root: Root, boost_root: Root, is_payload_present: bool, boost_value: uint64) -> Gwei: - boost = Gwei(0) +def compute_boost(store: Store, state: State, root: Root, slot: Slot, boost_root: Root, is_payload_present: bool, boost_value: uint64) -> Gwei: if boost_root == Root(): - return boost - (ancestor_root, is_ancestor_full) = get_ancestor(store, boost_root, store.blocks[root].slot) - if (ancestor_root == root) and (is_ancestor_full == is_payload_present): - committee_weight = get_total_active_balance(state) // SLOTS_PER_EPOCH - boost = (committee_weight * boost_value) // 100 - return boost + return Gwei(0) + (ancestor_root, is_ancestor_full) = get_ancestor(store, boost_root, slot) + if ancestor_root != root: + return Gwei(0) + if (slot != store.blocks[boost_root].slot) and (is_ancestor_full != is_payload_present): + return Gwei(0) + committee_weight = get_total_active_balance(state) // SLOTS_PER_EPOCH + return (committee_weight * boost_value) // 100 ``` ### Modified `get_weight` @@ -308,10 +309,10 @@ def get_weight(store: Store, root: Root, slot: Slot, is_payload_present: bool) - return attestation_score - proposer_score = compute_boost(store, state, root, store.proposer_boost_root, is_payload_present, PROPOSER_SCORE_BOOST) + proposer_score = compute_boost(store, state, root, slot, store.proposer_boost_root, is_payload_present, PROPOSER_SCORE_BOOST) - builder_reveal_score = compute_boost(store, state, root, store.payload_reveal_boost_root, is_payload_present, PAYLOAD_REVEAL_BOOST) - builder_withhold_score = compute_boost(store, state, root, store.payload_withhold_boost_root, is_payload_present, PAYLOAD_WITHHOLD_BOOST) + builder_reveal_score = compute_boost(store, state, root, slot, store.payload_reveal_boost_root, is_payload_present, PAYLOAD_REVEAL_BOOST) + builder_withhold_score = compute_boost(store, state, root, slot, store.payload_withhold_boost_root, is_payload_present, PAYLOAD_WITHHOLD_BOOST) return attestation_score + proposer_score + builder_reveal_score + builder_withhold_score ``` @@ -353,7 +354,7 @@ def get_head_no_il(store: Store) -> tuple[Root, bool]: # 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 - best_child = max(children, key=lambda child: (get_weight(store, child.root, child.slot, child.is_payload_present), is_payload_present(child.root), child.is_payload_present, child.slot, child.root)) + best_child = max(children, key=lambda child: (get_weight(store, child.root, child.slot, child.is_payload_present), is_payload_present(store, child.root), child.is_payload_present, child.slot, child.root)) if best_child.root == head_root: return (best_child.root, best_child.is_payload_present) head_root = best_child.root From 17b9e90f8a00cd5dd2cfbfa420c048e786247a52 Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 17 Apr 2024 16:11:06 -0300 Subject: [PATCH 095/112] use ChildNode across forkchoice --- specs/_features/epbs/fork-choice.md | 124 +++++++++++++++++++--------- 1 file changed, 85 insertions(+), 39 deletions(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 38f274c035..ac8b43206b 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -49,6 +49,19 @@ This is the modification of the fork choice accompanying the ePBS upgrade. | `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` @@ -97,6 +110,7 @@ class Store(object): 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] ``` @@ -216,6 +230,14 @@ def is_payload_present(store: Store, beacon_block_root: Root) -> bool: 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. @@ -234,9 +256,7 @@ def get_ancestor(store: Store, root: Root, slot: Slot) -> tuple[Root, bool]: parent = store.blocks[block.parent_root] if parent.slot > slot: return get_ancestor(store, block.parent_root, slot) - if block.body.signed_execution_payload_header.message.parent_block_hash == parent.body.signed_execution_payload_header.message.block_hash: - return (block.parent_root, True) - return (block.parent_root, False) + return (block.parent_root, is_parent_node_full(block)) ``` ### Modified `get_checkpoint_block` @@ -256,35 +276,72 @@ def get_checkpoint_block(store: Store, root: Root, epoch: Epoch) -> Root: ### `is_supporting_vote` ```python -def is_supporting_vote(store: Store, root: Root, slot: Slot, is_payload_present: bool, message: LatestMessage) -> bool: +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 ``root`` with the - payload contents indicated by ``is_payload_present`` as head during slot ``slot``. + 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 root == message.root: + 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 slot <= message.slot + return node.slot <= message.slot message_block = store.blocks[message.root] - if slot >= message_block.slot: + if node.slot >= message_block.slot: return False - (ancestor_root, is_ancestor_full) = get_ancestor(store, message.root, slot) - return (root == ancestor_root) and (is_payload_present == is_ancestor_full) + (ancestor_root, is_ancestor_full) = get_ancestor(store, message.root, node.slot) + return (node.root == ancestor_root) and (node.is_payload_present == is_ancestor_full) ``` -### New `compute_boost` +### 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_root, is_ancestor_full) = get_ancestor(store, store.proposer_boost_root, node.slot) + if ancestor_root != node.root: + return Gwei(0) + if (node.slot != store.blocks[store.proposer_boost_root].slot) and (is_ancestor_full != 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_root, is_ancestor_full) = 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: + is_ancestor_full = store.payload_withhold_boost_full + 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_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_boost(store: Store, state: State, root: Root, slot: Slot, boost_root: Root, is_payload_present: bool, boost_value: uint64) -> Gwei: - if boost_root == Root(): +def compute_reveal_boost(store: Store, state: State, node: ChildNode) -> Gwei: + if store.payload_reveal_boost_root == Root(): return Gwei(0) - (ancestor_root, is_ancestor_full) = get_ancestor(store, boost_root, slot) - if ancestor_root != root: + (ancestor_root, is_ancestor_full) = get_ancestor(store, store.payload_reveal_boost_root, node.slot) + if ancestor_root != node.root: return Gwei(0) - if (slot != store.blocks[boost_root].slot) and (is_ancestor_full != is_payload_present): + if node.slot == store.blocks[store.payload_reveal_boost_root].slot: + is_ancestor_full = 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 * boost_value) // 100 + return (committee_weight * PAYLOAD_REVEAL_BOOST) // 100 ``` ### Modified `get_weight` @@ -292,7 +349,7 @@ def compute_boost(store: Store, state: State, root: Root, slot: Slot, boost_root **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, root: Root, slot: Slot, is_payload_present: bool) -> Gwei: +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)) @@ -302,31 +359,18 @@ def get_weight(store: Store, root: Root, slot: Slot, is_payload_present: bool) - 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, root, slot, is_payload_present, store.latest_messages[i])) + and is_supporting_vote(store, node, store.latest_messages[i])) )) - if store.proposer_boost_root == Root(): - # Return only attestation score if ``proposer_boost_root`` is not set - return attestation_score - - proposer_score = compute_boost(store, state, root, slot, store.proposer_boost_root, is_payload_present, PROPOSER_SCORE_BOOST) - - builder_reveal_score = compute_boost(store, state, root, slot, store.payload_reveal_boost_root, is_payload_present, PAYLOAD_REVEAL_BOOST) - builder_withhold_score = compute_boost(store, state, root, slot, store.payload_withhold_boost_root, is_payload_present, PAYLOAD_WITHHOLD_BOOST) + # Compute boosts + node = ChildNode(root=root, slot=slot, is_payload_present=is_payload_present) + 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 `ChildNode` -Auxiliary class to consider `(block, slot, bool)` LMD voting - -```python -class ChildNode(Container): - root: Root - slot: Slot - is_payload_present: bool -``` - ### 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. @@ -354,7 +398,7 @@ def get_head_no_il(store: Store) -> tuple[Root, bool]: # 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 - best_child = max(children, key=lambda child: (get_weight(store, child.root, child.slot, child.is_payload_present), is_payload_present(store, child.root), child.is_payload_present, child.slot, child.root)) + 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 best_child.root == head_root: return (best_child.root, best_child.is_payload_present) head_root = best_child.root @@ -527,6 +571,7 @@ def on_tick_per_slot(store: Store, time: uint64) -> None: # 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 @@ -579,4 +624,5 @@ def on_payload_attestation_message(store: Store, 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) ``` From c63b81e29cb67bfb7f04e9a056f7064dd2f3b832 Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 17 Apr 2024 16:12:03 -0300 Subject: [PATCH 096/112] doctoc --- specs/_features/epbs/fork-choice.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index ac8b43206b..f40312b300 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -7,6 +7,9 @@ - [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) @@ -16,10 +19,13 @@ - [`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_boost`](#new-compute_boost) + - [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) From 5ff8d5498997be82bd255a80f4212626e3e482e1 Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 17 Apr 2024 16:29:19 -0300 Subject: [PATCH 097/112] modify get_ancestor to return a ChildNode --- specs/_features/epbs/fork-choice.md | 39 ++++++++++++++--------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index f40312b300..06f8fa6838 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -248,21 +248,21 @@ def is_parent_node_full(store: Store, block: BeaconBlock) -> bool: **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) -> tuple[Root, bool]: +def get_ancestor(store: Store, root: Root, slot: Slot) -> ChildNode: """ - Returns the beacon block root of the ancestor of the beacon block with ``root`` at ``slot`` and it also - returns ``true`` if it based on a full block and ``false`` otherwise. - If the beacon block with ``root`` is already at ``slot`` it returns it's PTC status. + 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`` it returns it's + PTC status instead of the actual payload content. """ block = store.blocks[root] if block.slot == slot: - return (root, store.is_payload_present(root)) + return ChildNode(root=root, slot=slot, is_payload_present=store.is_payload_present(root)) assert block.slot > slot parent = store.blocks[block.parent_root] if parent.slot > slot: return get_ancestor(store, block.parent_root, slot) - return (block.parent_root, is_parent_node_full(block)) + return ChildNode(root=block.parent_root, slot=parent.slot, is_payload_present=is_parent_node_full(block)) ``` ### Modified `get_checkpoint_block` @@ -274,8 +274,7 @@ 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) - (ancestor_root,_) = get_ancestor(store, root, epoch_first_slot) - return ancestor_root + return get_ancestor(store, root, epoch_first_slot).root ``` @@ -294,8 +293,8 @@ def is_supporting_vote(store: Store, node: ChildNode, message: LatestMessage) -> message_block = store.blocks[message.root] if node.slot >= message_block.slot: return False - (ancestor_root, is_ancestor_full) = get_ancestor(store, message.root, node.slot) - return (node.root == ancestor_root) and (node.is_payload_present == is_ancestor_full) + 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` @@ -304,10 +303,10 @@ This is a helper to compute the proposer boost. It applies the proposer boost to def compute_proposer_boost(store: Store, state: State, node: ChildNode) -> Gwei: if store.proposer_boost_root == Root(): return Gwei(0) - (ancestor_root, is_ancestor_full) = get_ancestor(store, store.proposer_boost_root, node.slot) - if ancestor_root != node.root: + ancestor = get_ancestor(store, store.proposer_boost_root, node.slot) + if ancestor.root != node.root: return Gwei(0) - if (node.slot != store.blocks[store.proposer_boost_root].slot) and (is_ancestor_full != node.is_payload_present): + if (node.slot != store.blocks[store.proposer_boost_root].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 @@ -320,12 +319,12 @@ This is a similar helper that applies for the withhold boost. In this case this def compute_withhold_boost(store: Store, state: State, node: ChildNode) -> Gwei: if store.payload_withhold_boost_root == Root(): return Gwei(0) - (ancestor_root, is_ancestor_full) = get_ancestor(store, store.payload_withold_boost_root, node.slot) - if ancestor_root != node.root: + 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: - is_ancestor_full = store.payload_withhold_boost_full - if is_ancestor_full != node.is_payload_present: + 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 @@ -339,11 +338,11 @@ This is a similar helper to the last two, the only difference is that the reveal def compute_reveal_boost(store: Store, state: State, node: ChildNode) -> Gwei: if store.payload_reveal_boost_root == Root(): return Gwei(0) - (ancestor_root, is_ancestor_full) = get_ancestor(store, store.payload_reveal_boost_root, node.slot) - if ancestor_root != node.root: + 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: - is_ancestor_full = True + 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 From f35cc683e244f764f63332ad3dd7056a5365a37c Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 17 Apr 2024 16:35:59 -0300 Subject: [PATCH 098/112] typo --- specs/_features/epbs/fork-choice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 06f8fa6838..dbbdc42e5b 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -256,7 +256,7 @@ def get_ancestor(store: Store, root: Root, slot: Slot) -> ChildNode: """ block = store.blocks[root] if block.slot == slot: - return ChildNode(root=root, slot=slot, is_payload_present=store.is_payload_present(root)) + return ChildNode(root=root, slot=slot, is_payload_present= is_payload_present(store, root)) assert block.slot > slot parent = store.blocks[block.parent_root] From 18c31e43858e083d53a3f75139867102f99df906 Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 17 Apr 2024 17:26:24 -0300 Subject: [PATCH 099/112] fix boosts for future slots --- specs/_features/epbs/fork-choice.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index dbbdc42e5b..92d4c663ce 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -251,14 +251,13 @@ def is_parent_node_full(store: Store, block: BeaconBlock) -> bool: 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`` it returns it's - PTC status instead of the actual payload content. + 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)) + if block.slot <= slot: + return ChildNode(root=root, slot=slot, is_payload_present=is_payload_present(store, root)) - assert block.slot > slot parent = store.blocks[block.parent_root] if parent.slot > slot: return get_ancestor(store, block.parent_root, slot) @@ -306,7 +305,11 @@ def compute_proposer_boost(store: Store, state: State, node: ChildNode) -> Gwei: ancestor = get_ancestor(store, store.proposer_boost_root, node.slot) if ancestor.root != node.root: return Gwei(0) - if (node.slot != store.blocks[store.proposer_boost_root].slot) and (ancestor.is_payload_present != node.is_payload_present): + 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 @@ -322,7 +325,7 @@ def compute_withhold_boost(store: Store, state: State, node: ChildNode) -> Gwei: 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: + 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) @@ -341,7 +344,7 @@ def compute_reveal_boost(store: Store, state: State, node: ChildNode) -> Gwei: 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: + 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) From ba0283c98ea06f63562e78424f64f1346f44060b Mon Sep 17 00:00:00 2001 From: Potuz Date: Thu, 18 Apr 2024 08:47:48 -0300 Subject: [PATCH 100/112] remove unnecessary node redefinition --- specs/_features/epbs/fork-choice.md | 1 - 1 file changed, 1 deletion(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 92d4c663ce..01e971bc96 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -371,7 +371,6 @@ def get_weight(store: Store, node: ChildNode) -> Gwei: )) # Compute boosts - node = ChildNode(root=root, slot=slot, is_payload_present=is_payload_present) 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) From 9edc2821e1e7b5f88c9cfa28e38a300bb6fab336 Mon Sep 17 00:00:00 2001 From: Potuz Date: Thu, 18 Apr 2024 09:00:20 -0300 Subject: [PATCH 101/112] descend only into right payload except at the justified checkpoint --- specs/_features/epbs/fork-choice.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 01e971bc96..7c5cae0f0e 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -394,7 +394,8 @@ def get_head_no_il(store: Store) -> tuple[Root, bool]: while True: children = [ ChildNode(root=root, slot=block.slot, is_payload_present=present) for (root, block) in blocks.items() - if block.parent_root == head_root for present in (True, False) if root in store.execution_payload_states or present == False + if block.parent_root == head_root and is_parent_node_full(store, block) == head_full + for present in (True, False) if root in store.execution_payload_states or present == False ] if len(children) == 0: return (head_root, head_full) From f3a035ab40124e9af57a994ef2539316e1348886 Mon Sep 17 00:00:00 2001 From: Potuz Date: Thu, 18 Apr 2024 09:06:56 -0300 Subject: [PATCH 102/112] update head root in get_head loop --- specs/_features/epbs/fork-choice.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 7c5cae0f0e..3293b0d054 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -394,7 +394,8 @@ def get_head_no_il(store: Store) -> tuple[Root, bool]: while True: children = [ ChildNode(root=root, slot=block.slot, is_payload_present=present) for (root, block) in blocks.items() - if block.parent_root == head_root and is_parent_node_full(store, block) == head_full + if block.parent_root == head_root and + is_parent_node_full(store, block) == head_full if root != store.justified_checkpoint.root for present in (True, False) if root in store.execution_payload_states or present == False ] if len(children) == 0: @@ -411,6 +412,8 @@ def get_head_no_il(store: Store) -> tuple[Root, bool]: return (best_child.root, best_child.is_payload_present) head_root = best_child.root head_full = best_child.is_payload_present + head_block = store.blocks[head_root] + head_slot = head_block.slot ``` ### Modified `get_head` From 6c60686cfaab83a023b7c4bb4f7381c6cab79f9f Mon Sep 17 00:00:00 2001 From: Potuz Date: Thu, 18 Apr 2024 12:12:50 -0300 Subject: [PATCH 103/112] update get_head to use ChildNodes --- specs/_features/epbs/fork-choice.md | 41 +++++++++++++---------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 3293b0d054..3b6ae8a11c 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -383,49 +383,46 @@ def get_weight(store: Store, node: ChildNode) -> Gwei: **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) -> tuple[Root, bool]: +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 - head_root = store.justified_checkpoint.root - head_block = store.blocks[head_root] - head_slot = head_block.slot - head_full = is_payload_present(store, head_root) + 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 == head_root and - is_parent_node_full(store, block) == head_full if root != store.justified_checkpoint.root + 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 (head_root, head_full) + return best_child # if we have children we consider the current head advanced as a possible head - children += [ChildNode(root=head_root, slot=head_slot + 1, head_full)] + 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 - 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 best_child.root == head_root: - return (best_child.root, best_child.is_payload_present) - head_root = best_child.root - head_full = best_child.is_payload_present - head_block = store.blocks[head_root] - head_slot = head_block.slot + 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) -> tuple[Root, bool]: - head_root, _ = get_head_no_il(store) - while not store.inclusion_list_available(head_root): - head_block = store.blocks[head_root] - head_root = head_block.parent_root - return head_root, store.is_payload_present(head_root) +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 From 502e98d56b6a60a1fca2bdf7e74036c3b821d13b Mon Sep 17 00:00:00 2001 From: Potuz Date: Thu, 18 Apr 2024 12:26:28 -0300 Subject: [PATCH 104/112] stranded on_block simplifications --- specs/_features/epbs/fork-choice.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index 3b6ae8a11c..e52f34316f 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -471,14 +471,12 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: 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 - parent_payload_hash = parent_header.block_hash - current_payload_parent_hash = header.parent_block_hash # Make a copy of the state to avoid mutability issues - if current_payload_parent_hash == parent_payload_hash: + 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 current_payload_parent_hash == parent_header.parent_block_hash + 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. @@ -507,7 +505,7 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None: # 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 current_payload_parent_hash == parent_header.parent_block_hash: + if not is_parent_node_full(store, block): store.inclusion_list_available = True # Notify the store about the payload_attestations in the block @@ -623,7 +621,7 @@ def on_payload_attestation_message(store: Store, if is_from_block && data.slot + 1 != get_current_slot(store): return time_into_slot = (store.time - store.genesis_time) % SECONDS_PER_SLOT - if time_into_slot >= SECONDS_PER_SLOT // INTERVALS_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 From 05fbb9ade089ab70fc339aca3f3f633631076fee Mon Sep 17 00:00:00 2001 From: Potuz Date: Thu, 18 Apr 2024 20:52:57 -0300 Subject: [PATCH 105/112] did not remove process_withdrawals --- specs/_features/epbs/beacon-chain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 04788e029a..cc7c0776e0 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -452,7 +452,7 @@ The post-state corresponding to a pre-state `state` and a signed execution paylo ```python def process_block(state: BeaconState, block: BeaconBlock) -> None: process_block_header(state, block) - removed process_withdrawals(state) [Modified in ePBS] + 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) From 1e9406df8ebb3d307daee64ce16d27baa8e340a7 Mon Sep 17 00:00:00 2001 From: terence tsao Date: Thu, 18 Apr 2024 19:31:29 -0700 Subject: [PATCH 106/112] Add engine_getInclusionListV1 to engine.md --- specs/_features/epbs/engine.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/specs/_features/epbs/engine.md b/specs/_features/epbs/engine.md index 77acc5a8c5..b7d95ae985 100644 --- a/specs/_features/epbs/engine.md +++ b/specs/_features/epbs/engine.md @@ -29,6 +29,9 @@ Engine API changes introduced in the ePBS fork - [Request](#request-3) - [Response](#response-3) - [Specification](#specification-3) + - [`engine_getInclusionListV1`](#engine_getinclusionlistv1) + - [Request](#request-4) + - [Response](#response-4) @@ -201,3 +204,17 @@ Refer to the specification for [`engine_getPayloadV2`](./shanghai.md#engine_getp 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 From 8883b29d8c1049155cf4827fd622956fa076ce42 Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 19 Apr 2024 10:55:14 -0300 Subject: [PATCH 107/112] coherent naming in engine API --- specs/_features/epbs/engine.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/_features/epbs/engine.md b/specs/_features/epbs/engine.md index b7d95ae985..8012f89bad 100644 --- a/specs/_features/epbs/engine.md +++ b/specs/_features/epbs/engine.md @@ -80,8 +80,8 @@ This structure contains the full list of transactions and the summary broadcast - `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. -- `parent_block_hash`: `DATA`, 32 Bytes. -- `proposer_index` : `QUANTITY`, 64 Bits. +- `parentHash`: `DATA`, 32 Bytes. +- `proposerIndex` : `QUANTITY`, 64 Bits. ### InclusionListStatusV1 From ae354164462643c3c0c9a8a3e8eb6a1285d09c38 Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 19 Apr 2024 14:10:49 -0300 Subject: [PATCH 108/112] fix unused constant --- specs/_features/epbs/beacon-chain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index cc7c0776e0..89f52f5d7f 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -100,7 +100,7 @@ At any given slot, the status of the blockchain's head may be either | Name | Value | Unit | Duration | | - | - | :-: | :-: | -| `MIN_SLOTS_FOR_INCLUSION_LISTS_REQUESTS` | `uint64(2)` | slots | 32 seconds # (New in ePBS) | +| `MIN_SLOTS_FOR_INCLUSION_LISTS_REQUESTS` | `uint64(2)` | slots | 24 seconds # (New in ePBS) | ### Max operations per block From eedf7fbe1e7bf3bd589acd8244f3dfac90d3d1b7 Mon Sep 17 00:00:00 2001 From: Potuz Date: Wed, 24 Apr 2024 13:32:17 -0300 Subject: [PATCH 109/112] Apply suggestions from Leonardo Arias Left out naming suggestions until a later stage. Co-authored-by: Leonardo Arias Co-authored-by: Ryan Schneider --- specs/_features/epbs/beacon-chain.md | 10 +++++----- specs/_features/epbs/builder.md | 14 +++++++------- specs/_features/epbs/engine.md | 4 ++-- specs/_features/epbs/fork-choice.md | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 89f52f5d7f..8bcae4f9c6 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -63,11 +63,11 @@ This is the beacon chain specification of the enshrined proposer builder separat *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 as opposed 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. +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 (eg. the current slot's proposer did not submit its block). -- An *empty* block from the current slot (eg. the proposer submitted a timely block, but the builder did not reveal the payload on time). +- 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 @@ -239,7 +239,7 @@ class BeaconBlockBody(Container): #### `ExecutionPayload` -**Note:** The `ExecutionPayload` is modified to contain a transaction inclusion list summary signed by the corresponding beacon block proposer +**Note:** The `ExecutionPayload` is modified to contain a transaction inclusion list summary signed by the corresponding beacon block proposer. ```python class ExecutionPayload(Container): @@ -379,7 +379,7 @@ def is_valid_indexed_payload_attestation(state: BeaconState, indexed_payload_att #### `is_parent_block_full` -This function returns true if the last committed payload header was fulfilled with a full payload, this can only happen when the parent block was full, that is 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. +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: diff --git a/specs/_features/epbs/builder.md b/specs/_features/epbs/builder.md index 5c480f8786..75bd69200c 100644 --- a/specs/_features/epbs/builder.md +++ b/specs/_features/epbs/builder.md @@ -20,7 +20,7 @@ With the ePBS Fork, the protocol includes new staked participants of the protoco ## 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 to reveal a full 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. +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. @@ -33,7 +33,7 @@ Prior to constructing a payload, the builder **MUST** have a full `InclusionList 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. +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`. @@ -86,20 +86,20 @@ def get_blob_sidecars(signed_block: SignedBeaconBlock, ### Constructing the execution payload envelope -When a valid `SignedBeaconBlock` has been published containing a signed commitment by the builder, the builder is later expected to broadcast the corresponding `SignedExecutionPayloadEnvelope` that fulfils this commitment. See below for a special case of an *honestly withheld payload*. +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`. +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`. +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 run `process_execution_payload(state, signed_envelope, verify=False)` and this function should not trigger an exception +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: diff --git a/specs/_features/epbs/engine.md b/specs/_features/epbs/engine.md index 8012f89bad..5989dde0ff 100644 --- a/specs/_features/epbs/engine.md +++ b/specs/_features/epbs/engine.md @@ -1,6 +1,6 @@ # Engine API -- ePBS -Engine API changes introduced in the ePBS fork +Engine API changes introduced in the ePBS fork. ## Table of contents @@ -69,7 +69,7 @@ This structure has the syntax of [`PayloadAttributesV2`](./shanghai.md#payloadat - `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 +- `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 diff --git a/specs/_features/epbs/fork-choice.md b/specs/_features/epbs/fork-choice.md index e52f34316f..35f5e510af 100644 --- a/specs/_features/epbs/fork-choice.md +++ b/specs/_features/epbs/fork-choice.md @@ -71,7 +71,7 @@ class ChildNode(Container): ## Helpers ### Modified `LatestMessage` -**Note:** The class is modified to keep track of the slot instead of the epoch +**Note:** The class is modified to keep track of the slot instead of the epoch. ```python @dataclass(eq=True, frozen=True) @@ -415,7 +415,7 @@ def get_head_no_il(store: Store) -> ChildNode: ``` ### 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 +`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: From 5ebda89ab36c0d29f1b4a4385c61c246cecf36bd Mon Sep 17 00:00:00 2001 From: terence tsao Date: Wed, 24 Apr 2024 18:42:08 -0700 Subject: [PATCH 110/112] Move exec payload header position to replace latest payload header --- specs/_features/epbs/beacon-chain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/_features/epbs/beacon-chain.md b/specs/_features/epbs/beacon-chain.md index 8bcae4f9c6..22f10de405 100644 --- a/specs/_features/epbs/beacon-chain.md +++ b/specs/_features/epbs/beacon-chain.md @@ -321,6 +321,7 @@ class BeaconState(Container): 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 @@ -333,7 +334,6 @@ class BeaconState(Container): latest_inclusion_list_slot: Slot # [New in ePBS] latest_block_hash: Hash32 # [New in ePBS] latest_full_slot: Slot # [New in ePBS] - execution_payload_header: ExecutionPayloadHeader # [New in ePBS] last_withdrawals_root: Root # [New in ePBS] ``` From 2decd4339592b0adbe8a38b140014dceaaae589f Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 17 May 2024 12:45:39 +0300 Subject: [PATCH 111/112] require the target blockroot of a PTC message is valid --- specs/_features/epbs/p2p-interface.md | 1 + 1 file changed, 1 insertion(+) diff --git a/specs/_features/epbs/p2p-interface.md b/specs/_features/epbs/p2p-interface.md index 44d47826a3..83228347d4 100644 --- a/specs/_features/epbs/p2p-interface.md +++ b/specs/_features/epbs/p2p-interface.md @@ -153,6 +153,7 @@ The following validations MUST pass before forwarding the `payload_attestation_m - _[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. From d0060c07e540019db9bd56d668d9584f47747c11 Mon Sep 17 00:00:00 2001 From: terence Date: Tue, 25 Jun 2024 05:00:23 -0700 Subject: [PATCH 112/112] Add fork logic for ePBS (#6) * Add fork logic --- specs/_features/epbs/fork.md | 133 +++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 specs/_features/epbs/fork.md 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 +``` +