From 8cf8802be42bd516ec54b8a3af710c2946c81e32 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Mon, 4 Mar 2024 15:41:54 +0100 Subject: [PATCH 1/8] Enable light client data backfill by tracking best `SyncAggregate` Beacon nodes can only compute light client data locally if they have the corresponding `BeaconState` available. This is not the case for blocks before the initially synced checkpoint state. The p2p-interface defines endpoints to sync light client data, but it only supports forward sync. To enable beacon nodes to backfill light client data, we must ensure that a malicious peer cannot convince us of fraudulent data. While it is possible to verify light client data against the locally backfilled blocks, blocks are not necessarily available anymore on libp2p as they are subject to `MIN_EPOCHS_FOR_BLOCK_REQUESTS`. Light client data stays relevant for more than 5 months, and without validating it against local block data it is impossible to distinguish canonical light client data from fraudulent light client data that eventually culminates in a shared history; the old periods in that case could still be manipulated. Furthermore, agreeing on canonical data improves caching performance and is relevant, e.g., for the portal network. To support efficient proof that a `LightClientUpdate` is canonical, it is proposed to minimally extend the `BeaconState` to track the best `SyncAggregate` of the current and previous sync committee period, according to an implementation-independent ranking function. The proposed ranking function is compatible with what consensus nodes implementing https://github.com/ethereum/consensus-specs/pull/3553 are already making available across libp2p and REST transports. It is based on and compatible with the `is_better_update` function in `specs/altair/light-client/sync-protocol.md`. There are three minor differences to `is_better_update`: 1. `is_better_update` runs in the LC, so runs without fork choice. It needs extra conditions to prefer older data over newer data. The `BeaconState` ranking function can use simpler logic. 2. The LC is always initialized from a post-Altair finalized checkpoint. This assumption does not hold in theoretical edge cases, requiring an extra guard for `ALTAIR_FORK_EPOCH` in the `BeaconState` function. 3. `is_better_update` has to deal with BNs serving incomplete data while they are still backfilling. This is not the case with `BeaconState`. Once the data is available in the `BeaconState`, a light client data backfill protocol could be defined that serves, for past periods: 1. A `LightClientUpdate` from requested `period` + 1 that proves that the entirety of `period` is finalized. 2. `BeaconState.historical_summaries[period].block_summary_root` at (1)'s `attested_header.beacon.state_root` + Merkle proof. 3. For each epoch's slot 0 block within requested `period`, the corresponding `LightClientHeader` + Merkle multi-proof for the block's inclusion into (2)'s `block_summary_root`. 4. For each of the entries from (3) with `beacon.slot` within `period`, the `current_sync_committee_branch` + Merkle proof for constructing `LightClientBootstrap`. 5. If (4) is not empty, the requested `period`'s `current_sync_committee`. 6. The best `LightClientUpdate` from `period`, if one exists, + Merkle proof that its `sync_aggregate` + `signature_slot` is selected as the canonical best one in (1)'s `attested_header.beacon.state_root`. Only the proof in (6) depends on `BeaconState` tracking the best light client data. This modification would enshrine the logic of a subset of `is_better_update`, but does not require adding any `LightClientXyz` data structures to the `BeaconState`. --- specs/electra/beacon-chain.md | 163 ++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 specs/electra/beacon-chain.md diff --git a/specs/electra/beacon-chain.md b/specs/electra/beacon-chain.md new file mode 100644 index 0000000000..0206fa5ba0 --- /dev/null +++ b/specs/electra/beacon-chain.md @@ -0,0 +1,163 @@ +# Electra -- The Beacon Chain + +**Notice**: This document is a work-in-progress for researchers and implementers. + +## Table of contents + + + + + +- [Containers](#containers) + - [New containers](#new-containers) + - [`SyncData`](#syncdata) + - [Extended containers](#extended-containers) + - [`BeaconState`](#beaconstate) +- [Beacon chain state transition function](#beacon-chain-state-transition-function) + - [Epoch processing](#epoch-processing) + - [Modified `process_sync_committee_updates`](#modified-process_sync_committee_updates) + - [Block processing](#block-processing) + - [New `process_best_sync_data`](#new-process_best_sync_data) + + + + +## Containers + +### New containers + +#### `SyncData` + +```python +class SyncData(Container): + # Sync committee aggregate signature + sync_aggregate: SyncAggregate + # Slot at which the aggregate signature was created + signature_slot: Slot +``` + +### Extended containers + +#### `BeaconState` + +```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] + # Sync history + previous_best_sync_data: SyncData # [New in Electra] + current_best_sync_data: SyncData # [New in Electra] + parent_block_has_sync_committee_finality: bool # [New in Electra] +``` + +## Beacon chain state transition function + +### Epoch processing + +#### Modified `process_sync_committee_updates` + +```python +def process_sync_committee_updates(state: BeaconState) -> None: + next_epoch = get_current_epoch(state) + Epoch(1) + if next_epoch % EPOCHS_PER_SYNC_COMMITTEE_PERIOD == 0: + state.current_sync_committee = state.next_sync_committee + state.next_sync_committee = get_next_sync_committee(state) + + # [New in Electra] + state.previous_best_sync_data = state.current_best_sync_data + state.current_best_sync_data = SyncData() + state.parent_block_has_sync_committee_finality = False +``` + +### Block processing + +```python +def process_block(state: BeaconState, block: BeaconBlock) -> None: + process_best_sync_data(state, block) # [New in Electra] + process_block_header(state, block) + process_withdrawals(state, block.body.execution_payload) + process_execution_payload(state, block.body, EXECUTION_ENGINE) + process_randao(state, block.body) + process_eth1_data(state, block.body) + process_operations(state, block.body) + process_sync_aggregate(state, block.body.sync_aggregate) +``` + +#### New `process_best_sync_data` + +```python +def process_best_sync_data(state: BeaconState, block: BeaconBlock) -> None: + signature_period = compute_sync_committee_period_at_slot(block.slot) + attested_period = compute_sync_committee_period_at_slot(state.latest_block_header.slot) + + # Track sync committee finality + old_has_sync_committee_finality = state.parent_block_has_sync_committee_finality + if state.parent_block_has_sync_committee_finality: + new_has_sync_committee_finality = True + elif state.finalized_checkpoint.epoch < ALTAIR_FORK_EPOCH: + new_has_sync_committee_finality = False + else: + finalized_period = compute_sync_committee_period(state.finalized_checkpoint.epoch) + new_has_sync_committee_finality = (finalized_period == attested_period) + state.parent_block_has_sync_committee_finality = new_has_sync_committee_finality + + # Track best sync data + if attested_period == signature_period: + max_active_participants = len(block.body.sync_aggregate.sync_committee_bits) + new_num_active_participants = sum(block.body.sync_aggregate.sync_committee_bits) + old_num_active_participants = sum(state.current_best_sync_data.sync_aggregate.sync_committee_bits) + new_has_supermajority = new_num_active_participants * 3 >= max_active_participants * 2 + old_has_supermajority = old_num_active_participants * 3 >= max_active_participants * 2 + if new_has_supermajority != old_has_supermajority: + is_better_sync_data = new_has_supermajority + elif not new_has_supermajority and new_num_active_participants != old_num_active_participants: + is_better_sync_data = new_num_active_participants > old_num_active_participants + elif new_has_sync_committee_finality != old_has_sync_committee_finality: + is_better_sync_data = new_has_sync_committee_finality + else: + is_better_sync_data = new_num_active_participants > old_num_active_participants + if is_better_sync_data: + state.current_best_sync_data = SyncData( + sync_aggregate=block.body.sync_aggregate, + signature_slot=block.slot, + ) +``` From 1e361e3cb7ccb204d960697e9e24ee9132f932b7 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Mon, 4 Mar 2024 16:12:50 +0100 Subject: [PATCH 2/8] Initialize to Infinity, so that `sync_aggregate` is always valid --- specs/electra/beacon-chain.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/specs/electra/beacon-chain.md b/specs/electra/beacon-chain.md index 0206fa5ba0..8bb9f28115 100644 --- a/specs/electra/beacon-chain.md +++ b/specs/electra/beacon-chain.md @@ -89,6 +89,21 @@ class BeaconState(Container): parent_block_has_sync_committee_finality: bool # [New in Electra] ``` +## Helper functions + +### `default_sync_data` + +```python +def default_sync_data() -> SyncData: + return SyncData( + sync_aggregate=SyncAggregate( + sync_committee_bits=Bitvector[SYNC_COMMITTEE_SIZE]() + sync_committee_signature=G2_POINT_AT_INFINITY, + ), + signature_slot=GENESIS_SLOT, + ) +``` + ## Beacon chain state transition function ### Epoch processing @@ -104,7 +119,7 @@ def process_sync_committee_updates(state: BeaconState) -> None: # [New in Electra] state.previous_best_sync_data = state.current_best_sync_data - state.current_best_sync_data = SyncData() + state.current_best_sync_data = default_sync_data() state.parent_block_has_sync_committee_finality = False ``` From 8a3287dca29aaf6e5ed05c0a0883118731539400 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Mon, 4 Mar 2024 16:13:24 +0100 Subject: [PATCH 3/8] Add fork logic --- specs/electra/fork.md | 144 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 specs/electra/fork.md diff --git a/specs/electra/fork.md b/specs/electra/fork.md new file mode 100644 index 0000000000..e1014f55ce --- /dev/null +++ b/specs/electra/fork.md @@ -0,0 +1,144 @@ +# Electra -- 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 Electra](#fork-to-electra) + - [Fork trigger](#fork-trigger) + - [Upgrading the state](#upgrading-the-state) + + + +## Introduction + +This document describes the process of Electra upgrade. + +## Configuration + +Warning: this configuration is not definitive. + +| Name | Value | +| - | - | +| `ELECTRA_FORK_VERSION` | `Version('0x05000000')` | +| `ELECTRA_FORK_EPOCH` | `Epoch(FAR_FUTURE_EPOCH)` | + +## 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 >= ELECTRA_FORK_EPOCH: + return ELECTRA_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 Electra + +### Fork trigger + +TBD. This fork is defined for testing purposes. +For now, we assume the condition will be triggered at epoch `ELECTRA_FORK_EPOCH`. + +Note that for the pure Electra networks, we don't apply `upgrade_to_electra` since it starts with Electra version logic. + +### Upgrading the state + +```python +def upgrade_to_electra(pre: deneb.BeaconState) -> BeaconState: + epoch = capella.get_current_epoch(pre) + latest_execution_payload_header = ExecutionPayloadHeader( + parent_hash=pre.latest_execution_payload_header.parent_hash, + fee_recipient=pre.latest_execution_payload_header.fee_recipient, + state_root=pre.latest_execution_payload_header.state_root, + receipts_root=pre.latest_execution_payload_header.receipts_root, + logs_bloom=pre.latest_execution_payload_header.logs_bloom, + prev_randao=pre.latest_execution_payload_header.prev_randao, + block_number=pre.latest_execution_payload_header.block_number, + gas_limit=pre.latest_execution_payload_header.gas_limit, + gas_used=pre.latest_execution_payload_header.gas_used, + timestamp=pre.latest_execution_payload_header.timestamp, + extra_data=pre.latest_execution_payload_header.extra_data, + base_fee_per_gas=pre.latest_execution_payload_header.base_fee_per_gas, + block_hash=pre.latest_execution_payload_header.block_hash, + transactions_root=pre.latest_execution_payload_header.transactions_root, + withdrawals_root=pre.latest_execution_payload_header.withdrawals_root, + blob_gas_used=pre.latest_execution_payload_header.blob_gas_used, # [Modified in Electra] + excess_blob_gas=pre.latest_execution_payload_header.excess_blob_gas, # [Modified in Electra] + ) + 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=ELECTRA_FORK_VERSION, # [Modified in Electra] + 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 + latest_execution_payload_header=latest_execution_payload_header, + # 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, + # Sync history + previous_best_sync_data=default_sync_data(), # [New in Electra] + current_best_sync_data=default_sync_data(), # [New in Electra] + parent_block_has_sync_committee_finality=False, # [New in Electra] + ) + + return post +``` From 5ad62b9ee6113db129ed8aa6cfce60b1096eba0f Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Mon, 4 Mar 2024 16:15:32 +0100 Subject: [PATCH 4/8] Lint --- specs/electra/beacon-chain.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/specs/electra/beacon-chain.md b/specs/electra/beacon-chain.md index 8bb9f28115..277483e034 100644 --- a/specs/electra/beacon-chain.md +++ b/specs/electra/beacon-chain.md @@ -13,6 +13,8 @@ - [`SyncData`](#syncdata) - [Extended containers](#extended-containers) - [`BeaconState`](#beaconstate) +- [Helper functions](#helper-functions) + - [`default_sync_data`](#default_sync_data) - [Beacon chain state transition function](#beacon-chain-state-transition-function) - [Epoch processing](#epoch-processing) - [Modified `process_sync_committee_updates`](#modified-process_sync_committee_updates) From 208e4a00a0deec9c537e556173543cce2813742e Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Mon, 4 Mar 2024 16:16:59 +0100 Subject: [PATCH 5/8] Use `deneb.get_current_epoch` in fork logic --- specs/electra/fork.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/electra/fork.md b/specs/electra/fork.md index e1014f55ce..83be4e8aef 100644 --- a/specs/electra/fork.md +++ b/specs/electra/fork.md @@ -68,7 +68,7 @@ Note that for the pure Electra networks, we don't apply `upgrade_to_electra` sin ```python def upgrade_to_electra(pre: deneb.BeaconState) -> BeaconState: - epoch = capella.get_current_epoch(pre) + epoch = deneb.get_current_epoch(pre) latest_execution_payload_header = ExecutionPayloadHeader( parent_hash=pre.latest_execution_payload_header.parent_hash, fee_recipient=pre.latest_execution_payload_header.fee_recipient, From 678859c2d06d71dbf61a3ad80b382d5859065915 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Mon, 4 Mar 2024 16:37:48 +0100 Subject: [PATCH 6/8] Handle network starting from Electra, and add remarks about forks --- specs/electra/fork.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/specs/electra/fork.md b/specs/electra/fork.md index 83be4e8aef..ae7f2b64cb 100644 --- a/specs/electra/fork.md +++ b/specs/electra/fork.md @@ -88,6 +88,14 @@ def upgrade_to_electra(pre: deneb.BeaconState) -> BeaconState: blob_gas_used=pre.latest_execution_payload_header.blob_gas_used, # [Modified in Electra] excess_blob_gas=pre.latest_execution_payload_header.excess_blob_gas, # [Modified in Electra] ) + # [New in Electra] + if pre.slot == deneb.GENESIS_SLOT: + has_sync_committee_finality = True + else: + # Finality may have advanced since the latest block, as slots and epochs were applied. + # As the finality at the latest block's post state is unknown, default to `False`. + # Only relevant if the fork is activated on a non-`SyncCommitteePeriod` boundary. + has_sync_committee_finality = False post = BeaconState( # Versioning genesis_time=pre.genesis_time, @@ -137,7 +145,7 @@ def upgrade_to_electra(pre: deneb.BeaconState) -> BeaconState: # Sync history previous_best_sync_data=default_sync_data(), # [New in Electra] current_best_sync_data=default_sync_data(), # [New in Electra] - parent_block_has_sync_committee_finality=False, # [New in Electra] + parent_block_has_sync_committee_finality=has_sync_committee_finality, # [New in Electra] ) return post From 5442c5ab3c91a8d026b6d0f71d4902b926f27796 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Mon, 4 Mar 2024 16:41:09 +0100 Subject: [PATCH 7/8] Simplify fork logic --- specs/electra/fork.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/specs/electra/fork.md b/specs/electra/fork.md index ae7f2b64cb..7e21bc4a22 100644 --- a/specs/electra/fork.md +++ b/specs/electra/fork.md @@ -88,14 +88,6 @@ def upgrade_to_electra(pre: deneb.BeaconState) -> BeaconState: blob_gas_used=pre.latest_execution_payload_header.blob_gas_used, # [Modified in Electra] excess_blob_gas=pre.latest_execution_payload_header.excess_blob_gas, # [Modified in Electra] ) - # [New in Electra] - if pre.slot == deneb.GENESIS_SLOT: - has_sync_committee_finality = True - else: - # Finality may have advanced since the latest block, as slots and epochs were applied. - # As the finality at the latest block's post state is unknown, default to `False`. - # Only relevant if the fork is activated on a non-`SyncCommitteePeriod` boundary. - has_sync_committee_finality = False post = BeaconState( # Versioning genesis_time=pre.genesis_time, @@ -145,7 +137,7 @@ def upgrade_to_electra(pre: deneb.BeaconState) -> BeaconState: # Sync history previous_best_sync_data=default_sync_data(), # [New in Electra] current_best_sync_data=default_sync_data(), # [New in Electra] - parent_block_has_sync_committee_finality=has_sync_committee_finality, # [New in Electra] + parent_block_has_sync_committee_finality=(pre.slot == deneb.GENESIS_SLOT), # [New in Electra] ) return post From 50232f959661f016e4f71c95d8493179e3ef2df3 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Mon, 4 Mar 2024 16:42:19 +0100 Subject: [PATCH 8/8] Constants are not versioned in the spec --- specs/electra/fork.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/electra/fork.md b/specs/electra/fork.md index 7e21bc4a22..62028fe932 100644 --- a/specs/electra/fork.md +++ b/specs/electra/fork.md @@ -137,7 +137,7 @@ def upgrade_to_electra(pre: deneb.BeaconState) -> BeaconState: # Sync history previous_best_sync_data=default_sync_data(), # [New in Electra] current_best_sync_data=default_sync_data(), # [New in Electra] - parent_block_has_sync_committee_finality=(pre.slot == deneb.GENESIS_SLOT), # [New in Electra] + parent_block_has_sync_committee_finality=(pre.slot == GENESIS_SLOT), # [New in Electra] ) return post