From 1432c3bd38d2d8f58436d2f3a46319a7d46ff5ba Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Wed, 17 Jul 2024 20:50:57 +0200 Subject: [PATCH] Adopt EIP-7688: Forward compatible consensus data structures EIP-4788 exposes the beacon root to smart contracts, but smart contracts using it need to be redeployed / upgraded whenever the indexing changes during a fork, even if that fork does not touch any used functionality. This problem expands further to bridges on other blockchains, or even into wallet apps on a phone that verify data from the beacon chain instead of trusting the server. It is quite unrealistic to expect such projects to all align their release cadence with Ethereum's forks. EIP-7688 fixes this by defining forward compatibility for beacon chain data structures. Electra `Profile` retain their Merkleization even when rebased to `StableContainer` definitions from future forks, enabling decentralized protocols to drop the requirement for trusted parties to periodically upgrade beacon state proof verifiers. (+1 squashed commit) Squashed commits: [8b92f7b0f] Adopt EIP-7688: Forward compatible consensus data structures EIP-4788 exposes the beacon root to smart contracts, but smart contracts using it need to be redeployed / upgraded whenever the indexing changes during a fork, even if that fork does not touch any used functionality. This problem expands further to bridges on other blockchains, or even into wallet apps on a phone that verify data from the beacon chain instead of trusting the server. It is quite unrealistic to expect such projects to all align their release cadence with Ethereum's forks. EIP-7688 fixes this by defining forward compatibility for beacon chain data structures. Electra `Profile` retain their Merkleization even when rebased to `StableContainer` definitions from future forks, enabling decentralized protocols to drop the requirement for trusted parties to periodically upgrade beacon state proof verifiers. --- presets/mainnet/electra.yaml | 5 + presets/minimal/electra.yaml | 5 + pysetup/helpers.py | 4 + pysetup/spec_builders/deneb.py | 5 - pysetup/spec_builders/electra.py | 14 +- setup.py | 6 +- specs/capella/light-client/full-node.md | 2 +- specs/capella/light-client/sync-protocol.md | 16 +- specs/deneb/light-client/full-node.md | 2 +- specs/deneb/light-client/sync-protocol.md | 5 +- specs/deneb/p2p-interface.md | 15 +- specs/electra/beacon-chain.md | 217 +++++++++++++++++- specs/electra/light-client/fork.md | 3 +- specs/electra/light-client/full-node.md | 2 +- specs/electra/light-client/sync-protocol.md | 30 ++- specs/electra/p2p-interface.md | 22 ++ .../light_client/test_single_merkle_proof.py | 5 +- .../deneb/unittests/test_config_invariants.py | 8 +- .../eth2spec/test/helpers/light_client.py | 6 + .../pyspec/eth2spec/utils/ssz/ssz_typing.py | 1 + 20 files changed, 330 insertions(+), 43 deletions(-) diff --git a/presets/mainnet/electra.yaml b/presets/mainnet/electra.yaml index bb97cfa445..77e618d929 100644 --- a/presets/mainnet/electra.yaml +++ b/presets/mainnet/electra.yaml @@ -43,3 +43,8 @@ MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 16 # --------------------------------------------------------------- # 2**3 ( = 8) pending withdrawals MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 8 + +# Misc +# --------------------------------------------------------------- +# `floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK)` = 7 + 1 + 12 = 20 +KZG_COMMITMENT_INCLUSION_PROOF_DEPTH_ELECTRA: 20 diff --git a/presets/minimal/electra.yaml b/presets/minimal/electra.yaml index ef1ce494d8..5ca228d32b 100644 --- a/presets/minimal/electra.yaml +++ b/presets/minimal/electra.yaml @@ -43,3 +43,8 @@ MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 2 # --------------------------------------------------------------- # 2**0 ( = 1) pending withdrawals MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 1 + +# Misc +# --------------------------------------------------------------- +# [customized] `floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK)` = 7 + 1 + 4 = 12 +KZG_COMMITMENT_INCLUSION_PROOF_DEPTH_ELECTRA: 12 diff --git a/pysetup/helpers.py b/pysetup/helpers.py index 589ae6ab58..ab9855f098 100644 --- a/pysetup/helpers.py +++ b/pysetup/helpers.py @@ -194,6 +194,10 @@ def dependency_order_class_objects(objects: Dict[str, str], custom_types: Dict[s for key, value in items: dependencies = [] for line in value.split('\n'): + profile_match = re.match(r'class\s+\w+\s*\(Profile\[\s*(\w+)\s*\]\s*\)\s*:', line) + if profile_match is not None: + dependencies.append(profile_match.group(1)) # SSZ `Profile` base + continue if not re.match(r'\s+\w+: .+', line): continue # skip whitespace etc. line = line[line.index(':') + 1:] # strip of field name diff --git a/pysetup/spec_builders/deneb.py b/pysetup/spec_builders/deneb.py index 436ae70b1d..7585ee31dc 100644 --- a/pysetup/spec_builders/deneb.py +++ b/pysetup/spec_builders/deneb.py @@ -72,10 +72,5 @@ def hardcoded_custom_type_dep_constants(cls, spec_object) -> Dict[str, str]: 'FIELD_ELEMENTS_PER_BLOB': spec_object.preset_vars['FIELD_ELEMENTS_PER_BLOB'].value, 'MAX_BLOBS_PER_BLOCK': spec_object.config_vars['MAX_BLOBS_PER_BLOCK'].value, 'MAX_BLOB_COMMITMENTS_PER_BLOCK': spec_object.preset_vars['MAX_BLOB_COMMITMENTS_PER_BLOCK'].value, - } - - @classmethod - def hardcoded_func_dep_presets(cls, spec_object) -> Dict[str, str]: - return { 'KZG_COMMITMENT_INCLUSION_PROOF_DEPTH': spec_object.preset_vars['KZG_COMMITMENT_INCLUSION_PROOF_DEPTH'].value, } diff --git a/pysetup/spec_builders/electra.py b/pysetup/spec_builders/electra.py index ca02ee927c..cd8e9c9d40 100644 --- a/pysetup/spec_builders/electra.py +++ b/pysetup/spec_builders/electra.py @@ -10,12 +10,20 @@ class ElectraSpecBuilder(BaseSpecBuilder): def imports(cls, preset_name: str): return f''' from eth2spec.deneb import {preset_name} as deneb +from eth2spec.utils.ssz.ssz_typing import StableContainer, Profile ''' @classmethod def hardcoded_ssz_dep_constants(cls) -> Dict[str, str]: return { - 'FINALIZED_ROOT_GINDEX_ELECTRA': 'GeneralizedIndex(169)', - 'CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA': 'GeneralizedIndex(86)', - 'NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA': 'GeneralizedIndex(87)', + 'FINALIZED_ROOT_GINDEX_ELECTRA': 'GeneralizedIndex(553)', + 'CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA': 'GeneralizedIndex(278)', + 'NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA': 'GeneralizedIndex(279)', + 'EXECUTION_PAYLOAD_GINDEX_ELECTRA': 'GeneralizedIndex(137)', + } + + @classmethod + def hardcoded_custom_type_dep_constants(cls, spec_object) -> Dict[str, str]: + return { + 'KZG_COMMITMENT_INCLUSION_PROOF_DEPTH_ELECTRA': spec_object.preset_vars['KZG_COMMITMENT_INCLUSION_PROOF_DEPTH_ELECTRA'].value, } diff --git a/setup.py b/setup.py index 539db215b7..4011fb4d62 100644 --- a/setup.py +++ b/setup.py @@ -173,7 +173,7 @@ def _update_constant_vars_with_kzg_setups(constant_vars, preset_name): constant_vars['KZG_SETUP_G1_MONOMIAL'] = VariableDefinition(constant_vars['KZG_SETUP_G1_MONOMIAL'].value, str(kzg_setups[0]), comment, None) constant_vars['KZG_SETUP_G1_LAGRANGE'] = VariableDefinition(constant_vars['KZG_SETUP_G1_LAGRANGE'].value, str(kzg_setups[1]), comment, None) constant_vars['KZG_SETUP_G2_MONOMIAL'] = VariableDefinition(constant_vars['KZG_SETUP_G2_MONOMIAL'].value, str(kzg_setups[2]), comment, None) - + def get_spec(file_name: Path, preset: Dict[str, str], config: Dict[str, str], preset_name=str) -> SpecObject: functions: Dict[str, str] = {} @@ -227,7 +227,7 @@ def get_spec(file_name: Path, preset: Dict[str, str], config: Dict[str, str], pr raise if parent_class: - assert parent_class == "Container" + assert parent_class in ["Container", "StableContainer", "Profile"] # NOTE: trim whitespace from spec ssz_objects[current_name] = "\n".join(line.rstrip() for line in source.splitlines()) else: @@ -552,7 +552,7 @@ def run(self): "pycryptodome==3.15.0", "py_ecc==6.0.0", "milagro_bls_binding==1.9.0", - "remerkleable==0.1.28", + "remerkleable @ git+https://github.com/etan-status/remerkleable@dev/etan/sc-default", "trie==2.0.2", RUAMEL_YAML_VERSION, "lru-dict==1.2.0", diff --git a/specs/capella/light-client/full-node.md b/specs/capella/light-client/full-node.md index 319fb1c944..914b3529a4 100644 --- a/specs/capella/light-client/full-node.md +++ b/specs/capella/light-client/full-node.md @@ -47,7 +47,7 @@ def block_to_light_client_header(block: SignedBeaconBlock) -> LightClientHeader: withdrawals_root=hash_tree_root(payload.withdrawals), ) execution_branch = ExecutionBranch( - compute_merkle_proof(block.message.body, EXECUTION_PAYLOAD_GINDEX)) + compute_merkle_proof(block.message.body, execution_payload_gindex_at_slot(block.message.slot))) else: # Note that during fork transitions, `finalized_header` may still point to earlier forks. # While Bellatrix blocks also contain an `ExecutionPayload` (minus `withdrawals_root`), diff --git a/specs/capella/light-client/sync-protocol.md b/specs/capella/light-client/sync-protocol.md index b241b21378..888bc89041 100644 --- a/specs/capella/light-client/sync-protocol.md +++ b/specs/capella/light-client/sync-protocol.md @@ -14,6 +14,7 @@ - [Containers](#containers) - [Modified `LightClientHeader`](#modified-lightclientheader) - [Helper functions](#helper-functions) + - [`execution_payload_gindex_at_slot`](#execution_payload_gindex_at_slot) - [`get_lc_execution_root`](#get_lc_execution_root) - [Modified `is_valid_light_client_header`](#modified-is_valid_light_client_header) @@ -55,6 +56,16 @@ class LightClientHeader(Container): ## Helper functions +### `execution_payload_gindex_at_slot` + +```python +def execution_payload_gindex_at_slot(slot: Slot) -> GeneralizedIndex: + epoch = compute_epoch_at_slot(slot) + assert epoch >= CAPELLA_FORK_EPOCH + + return EXECUTION_PAYLOAD_GINDEX +``` + ### `get_lc_execution_root` ```python @@ -79,11 +90,10 @@ def is_valid_light_client_header(header: LightClientHeader) -> bool: and header.execution_branch == ExecutionBranch() ) - return is_valid_merkle_branch( + return is_valid_normalized_merkle_branch( leaf=get_lc_execution_root(header), branch=header.execution_branch, - depth=floorlog2(EXECUTION_PAYLOAD_GINDEX), - index=get_subtree_index(EXECUTION_PAYLOAD_GINDEX), + gindex=execution_payload_gindex_at_slot(header.beacon.slot), root=header.beacon.body_root, ) ``` diff --git a/specs/deneb/light-client/full-node.md b/specs/deneb/light-client/full-node.md index 424723667c..141a2edeb3 100644 --- a/specs/deneb/light-client/full-node.md +++ b/specs/deneb/light-client/full-node.md @@ -53,7 +53,7 @@ def block_to_light_client_header(block: SignedBeaconBlock) -> LightClientHeader: execution_header.excess_blob_gas = payload.excess_blob_gas execution_branch = ExecutionBranch( - compute_merkle_proof(block.message.body, EXECUTION_PAYLOAD_GINDEX)) + compute_merkle_proof(block.message.body, execution_payload_gindex_at_slot(block.message.slot))) else: # Note that during fork transitions, `finalized_header` may still point to earlier forks. # While Bellatrix blocks also contain an `ExecutionPayload` (minus `withdrawals_root`), diff --git a/specs/deneb/light-client/sync-protocol.md b/specs/deneb/light-client/sync-protocol.md index 38aa3897b3..8a33e12346 100644 --- a/specs/deneb/light-client/sync-protocol.md +++ b/specs/deneb/light-client/sync-protocol.md @@ -77,11 +77,10 @@ def is_valid_light_client_header(header: LightClientHeader) -> bool: and header.execution_branch == ExecutionBranch() ) - return is_valid_merkle_branch( + return is_valid_normalized_merkle_branch( leaf=get_lc_execution_root(header), branch=header.execution_branch, - depth=floorlog2(EXECUTION_PAYLOAD_GINDEX), - index=get_subtree_index(EXECUTION_PAYLOAD_GINDEX), + gindex=execution_payload_gindex_at_slot(header.beacon.slot), root=header.beacon.body_root, ) ``` diff --git a/specs/deneb/p2p-interface.md b/specs/deneb/p2p-interface.md index 41e2fa9d3f..f2553fd7a9 100644 --- a/specs/deneb/p2p-interface.md +++ b/specs/deneb/p2p-interface.md @@ -14,6 +14,7 @@ The specification of these changes continues in the same format as the network s - [Constant](#constant) - [Preset](#preset) - [Configuration](#configuration) + - [Custom types](#custom-types) - [Containers](#containers) - [`BlobSidecar`](#blobsidecar) - [`BlobIdentifier`](#blobidentifier) @@ -66,6 +67,12 @@ The specification of these changes continues in the same format as the network s | `MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS` | `2**12` (= 4096 epochs, ~18 days) | The minimum epoch range over which a node must serve blob sidecars | | `BLOB_SIDECAR_SUBNET_COUNT` | `6` | The number of blob sidecar subnets used in the gossipsub protocol. | +### Custom types + +| Name | SSZ equivalent | Description | +| - | - | - | +| `KZGCommitmentInclusionProof` | `Vector[Bytes32, KZG_COMMITMENT_INCLUSION_PROOF_DEPTH]` | Merkle branch of a single `blob_kzg_commitments` list item within `BeaconBlockBody` | + ### Containers #### `BlobSidecar` @@ -79,7 +86,7 @@ class BlobSidecar(Container): 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] + kzg_commitment_inclusion_proof: KZGCommitmentInclusionProof ``` #### `BlobIdentifier` @@ -98,12 +105,12 @@ class BlobIdentifier(Container): ```python def verify_blob_sidecar_inclusion_proof(blob_sidecar: BlobSidecar) -> bool: - gindex = get_subtree_index(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments', blob_sidecar.index)) + gindex = get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments', 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, + depth=floorlog2(gindex), + index=get_subtree_index(gindex), root=blob_sidecar.signed_block_header.message.body_root, ) ``` diff --git a/specs/electra/beacon-chain.md b/specs/electra/beacon-chain.md index d9185b2030..eb3b2699a2 100644 --- a/specs/electra/beacon-chain.md +++ b/specs/electra/beacon-chain.md @@ -10,6 +10,7 @@ - [Introduction](#introduction) - [Constants](#constants) + - [`StableContainer` capacities](#stablecontainer-capacities) - [Misc](#misc) - [Withdrawal prefixes](#withdrawal-prefixes) - [Domains](#domains) @@ -30,9 +31,17 @@ - [`WithdrawalRequest`](#withdrawalrequest) - [`ConsolidationRequest`](#consolidationrequest) - [`PendingConsolidation`](#pendingconsolidation) + - [`StableContainer` definitions](#stablecontainer-definitions) + - [`StableAttestation`](#stableattestation) + - [`StableIndexedAttestation`](#stableindexedattestation) + - [`StableAttesterSlashing`](#stableattesterslashing) + - [`StableExecutionPayload`](#stableexecutionpayload) + - [`StableExecutionPayloadHeader`](#stableexecutionpayloadheader) + - [`StableBeaconBlockBody`](#stablebeaconblockbody) + - [`StableBeaconState`](#stablebeaconstate) - [Modified Containers](#modified-containers) - [`AttesterSlashing`](#attesterslashing) - - [Extended Containers](#extended-containers) + - [`Profile` definitions](#profile-definitions) - [`Attestation`](#attestation) - [`IndexedAttestation`](#indexedattestation) - [`BeaconBlockBody`](#beaconblockbody) @@ -116,6 +125,16 @@ Electra is a consensus-layer upgrade containing a number of features. Including: The following values are (non-configurable) constants used throughout the specification. +### `StableContainer` capacities + +| Name | Value | Description | +| - | - | - | +| `MAX_ATTESTATION_FIELDS` | `uint64(2**3)` (= 8) | Maximum number of fields to which `StableAttestation` can ever grow in the future | +| `MAX_INDEXED_ATTESTATION_FIELDS` | `uint64(2**3)` (= 8) | Maximum number of fields to which `StableIndexedAttestation` can ever grow in the future | +| `MAX_EXECUTION_PAYLOAD_FIELDS` | `uint64(2**6)` (= 64) | Maximum number of fields to which `StableExecutionPayload` can ever grow in the future | +| `MAX_BEACON_BLOCK_BODY_FIELDS` | `uint64(2**6)` (= 64) | Maximum number of fields to which `StableBeaconBlockBody` can ever grow in the future | +| `MAX_BEACON_STATE_FIELDS` | `uint64(2**7)` (= 128) | Maximum number of fields to which `StableBeaconState` can ever grow in the future | + ### Misc | Name | Value | Description | @@ -226,6 +245,7 @@ class PendingPartialWithdrawal(Container): amount: Gwei withdrawable_epoch: Epoch ``` + #### `WithdrawalRequest` *Note*: The container is new in EIP7251:EIP7002. @@ -258,6 +278,185 @@ class PendingConsolidation(Container): target_index: ValidatorIndex ``` +### `StableContainer` definitions + +These definitions provide EIP-7495 forward-compatibility guarantees. `Profile` based on these `StableContainer` definitions retain their Merkleization when rebased to `StableContainer` definitions of future forks. + +#### `StableAttestation` + +*Note*: The `StableContainer` is new in EIP7688. + +```python +class StableAttestation(StableContainer[MAX_ATTESTATION_FIELDS]): + aggregation_bits: Optional[Bitlist[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT]] + data: Optional[AttestationData] + signature: Optional[BLSSignature] + committee_bits: Optional[Bitvector[MAX_COMMITTEES_PER_SLOT]] +``` + +#### `StableIndexedAttestation` + +*Note*: The `StableContainer` is new in EIP7688. + +```python +class StableIndexedAttestation(StableContainer[MAX_INDEXED_ATTESTATION_FIELDS]): + attesting_indices: Optional[List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT]] + data: Optional[AttestationData] + signature: Optional[BLSSignature] +``` + +#### `StableAttesterSlashing` + +*Note*: The `StableContainer` is new in EIP7688. + +```python +class StableAttesterSlashing(Container): + attestation_1: StableIndexedAttestation + attestation_2: StableIndexedAttestation +``` + +#### `StableExecutionPayload` + +*Note*: The `StableContainer` is new in EIP7688. + +```python +class StableExecutionPayload(StableContainer[MAX_EXECUTION_PAYLOAD_FIELDS]): + parent_hash: Optional[Hash32] + fee_recipient: Optional[ExecutionAddress] # 'beneficiary' in the yellow paper + state_root: Optional[Bytes32] + receipts_root: Optional[Bytes32] + logs_bloom: Optional[ByteVector[BYTES_PER_LOGS_BLOOM]] + prev_randao: Optional[Bytes32] # 'difficulty' in the yellow paper + block_number: Optional[uint64] # 'number' in the yellow paper + gas_limit: Optional[uint64] + gas_used: Optional[uint64] + timestamp: Optional[uint64] + extra_data: Optional[ByteList[MAX_EXTRA_DATA_BYTES]] + base_fee_per_gas: Optional[uint256] + block_hash: Optional[Hash32] # Hash of execution block + transactions: Optional[List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD]] + withdrawals: Optional[List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD]] # [New in Capella] + blob_gas_used: Optional[uint64] # [New in Deneb:EIP4844] + excess_blob_gas: Optional[uint64] # [New in Deneb:EIP4844] + deposit_requests: Optional[List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD]] # [New in Electra:EIP6110] + # [New in Electra:EIP7002:EIP7251] + withdrawal_requests: Optional[List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD]] + # [New in Electra:EIP7251] + consolidation_requests: Optional[List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD]] +``` + +#### `StableExecutionPayloadHeader` + +*Note*: The `StableContainer` is new in EIP7688. + +```python +class StableExecutionPayloadHeader(StableContainer[MAX_EXECUTION_PAYLOAD_FIELDS]): + parent_hash: Optional[Hash32] + fee_recipient: Optional[ExecutionAddress] + state_root: Optional[Bytes32] + receipts_root: Optional[Bytes32] + logs_bloom: Optional[ByteVector[BYTES_PER_LOGS_BLOOM]] + prev_randao: Optional[Bytes32] + block_number: Optional[uint64] + gas_limit: Optional[uint64] + gas_used: Optional[uint64] + timestamp: Optional[uint64] + extra_data: Optional[ByteList[MAX_EXTRA_DATA_BYTES]] + base_fee_per_gas: Optional[uint256] + block_hash: Optional[Hash32] # Hash of execution block + transactions_root: Optional[Root] + withdrawals_root: Optional[Root] # [New in Capella] + blob_gas_used: Optional[uint64] # [New in Deneb:EIP4844] + excess_blob_gas: Optional[uint64] # [New in Deneb:EIP4844] + deposit_requests_root: Optional[Root] # [New in Electra:EIP6110] + withdrawal_requests_root: Optional[Root] # [New in Electra:EIP7002:EIP7251] + consolidation_requests_root: Optional[Root] # [New in Electra:EIP7251] +``` + +#### `StableBeaconBlockBody` + +*Note*: The `StableContainer` is new in EIP7688. + +```python +class StableBeaconBlockBody(StableContainer[MAX_BEACON_BLOCK_BODY_FIELDS]): + randao_reveal: Optional[BLSSignature] + eth1_data: Optional[Eth1Data] # Eth1 data vote + graffiti: Optional[Bytes32] # Arbitrary data + proposer_slashings: Optional[List[ProposerSlashing, MAX_PROPOSER_SLASHINGS]] + # [Modified in Electra:EIP7549] + attester_slashings: Optional[List[StableAttesterSlashing, MAX_ATTESTER_SLASHINGS_ELECTRA]] + attestations: Optional[List[StableAttestation, MAX_ATTESTATIONS_ELECTRA]] # [Modified in Electra:EIP7549] + deposits: Optional[List[Deposit, MAX_DEPOSITS]] + voluntary_exits: Optional[List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS]] + sync_aggregate: Optional[SyncAggregate] # [New in Altair] + execution_payload: Optional[StableExecutionPayload] # [New in Bellatrix] + # [New in Capella] + bls_to_execution_changes: Optional[List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES]] + blob_kzg_commitments: Optional[List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK]] # [New in Deneb:EIP4844] +``` + +#### `StableBeaconState` + +*Note*: The `StableContainer` is new in EIP7688. + +```python +class StableBeaconState(StableContainer[MAX_BEACON_STATE_FIELDS]): + # Versioning + genesis_time: Optional[uint64] + genesis_validators_root: Optional[Root] + slot: Optional[Slot] + fork: Optional[Fork] + # History + latest_block_header: Optional[BeaconBlockHeader] + block_roots: Optional[Vector[Root, SLOTS_PER_HISTORICAL_ROOT]] + state_roots: Optional[Vector[Root, SLOTS_PER_HISTORICAL_ROOT]] + # Frozen in Capella, replaced by historical_summaries + historical_roots: Optional[List[Root, HISTORICAL_ROOTS_LIMIT]] + # Eth1 + eth1_data: Optional[Eth1Data] + eth1_data_votes: Optional[List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH]] + eth1_deposit_index: Optional[uint64] + # Registry + validators: Optional[List[Validator, VALIDATOR_REGISTRY_LIMIT]] + balances: Optional[List[Gwei, VALIDATOR_REGISTRY_LIMIT]] + # Randomness + randao_mixes: Optional[Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR]] + # Slashings + slashings: Optional[Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR]] # Per-epoch sums of slashed effective balances + # Participation + previous_epoch_participation: Optional[List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]] # [Modified in Altair] + current_epoch_participation: Optional[List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]] # [Modified in Altair] + # Finality + justification_bits: Optional[Bitvector[JUSTIFICATION_BITS_LENGTH]] # Bit set for every recent justified epoch + previous_justified_checkpoint: Optional[Checkpoint] + current_justified_checkpoint: Optional[Checkpoint] + finalized_checkpoint: Optional[Checkpoint] + # Inactivity + inactivity_scores: Optional[List[uint64, VALIDATOR_REGISTRY_LIMIT]] # [New in Altair] + # Sync + current_sync_committee: Optional[SyncCommittee] # [New in Altair] + next_sync_committee: Optional[SyncCommittee] # [New in Altair] + # Execution + latest_execution_payload_header: Optional[StableExecutionPayloadHeader] # [New in Bellatrix] + # Withdrawals + next_withdrawal_index: Optional[WithdrawalIndex] # [New in Capella] + next_withdrawal_validator_index: Optional[ValidatorIndex] # [New in Capella] + # Deep history valid from Capella onwards + historical_summaries: Optional[List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT]] # [New in Capella] + deposit_requests_start_index: Optional[uint64] # [New in Electra:EIP6110] + deposit_balance_to_consume: Optional[Gwei] # [New in Electra:EIP7251] + exit_balance_to_consume: Optional[Gwei] # [New in Electra:EIP7251] + earliest_exit_epoch: Optional[Epoch] # [New in Electra:EIP7251] + consolidation_balance_to_consume: Optional[Gwei] # [New in Electra:EIP7251] + earliest_consolidation_epoch: Optional[Epoch] # [New in Electra:EIP7251] + # [New in Electra:EIP7251] + pending_balance_deposits: Optional[List[PendingBalanceDeposit, PENDING_BALANCE_DEPOSITS_LIMIT]] + # [New in Electra:EIP7251] + pending_partial_withdrawals: Optional[List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT]] + # [New in Electra:EIP7251] + pending_consolidations: Optional[List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT]] +``` + ### Modified Containers #### `AttesterSlashing` @@ -268,12 +467,12 @@ class AttesterSlashing(Container): attestation_2: IndexedAttestation # [Modified in Electra:EIP7549] ``` -### Extended Containers +### `Profile` definitions #### `Attestation` ```python -class Attestation(Container): +class Attestation(Profile[StableAttestation]): aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT] # [Modified in Electra:EIP7549] data: AttestationData signature: BLSSignature @@ -283,7 +482,7 @@ class Attestation(Container): #### `IndexedAttestation` ```python -class IndexedAttestation(Container): +class IndexedAttestation(Profile[StableIndexedAttestation]): # [Modified in Electra:EIP7549] attesting_indices: List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT] data: AttestationData @@ -293,7 +492,7 @@ class IndexedAttestation(Container): #### `BeaconBlockBody` ```python -class BeaconBlockBody(Container): +class BeaconBlockBody(Profile[StableBeaconBlockBody]): randao_reveal: BLSSignature eth1_data: Eth1Data # Eth1 data vote graffiti: Bytes32 # Arbitrary data @@ -313,7 +512,7 @@ class BeaconBlockBody(Container): #### `ExecutionPayload` ```python -class ExecutionPayload(Container): +class ExecutionPayload(Profile[StableExecutionPayload]): # Execution block header fields parent_hash: Hash32 fee_recipient: ExecutionAddress @@ -343,7 +542,7 @@ class ExecutionPayload(Container): #### `ExecutionPayloadHeader` ```python -class ExecutionPayloadHeader(Container): +class ExecutionPayloadHeader(Profile[StableExecutionPayloadHeader]): # Execution block header fields parent_hash: Hash32 fee_recipient: ExecutionAddress @@ -371,7 +570,7 @@ class ExecutionPayloadHeader(Container): #### `BeaconState` ```python -class BeaconState(Container): +class BeaconState(Profile[StableBeaconState]): # Versioning genesis_time: uint64 genesis_validators_root: Root @@ -874,7 +1073,7 @@ def process_pending_balance_deposits(state: BeaconState) -> None: if processed_amount + deposit.amount > available_for_processing: break # Deposit fits in the churn, process it. Increase balance and consume churn. - else: + else: increase_balance(state, deposit.index, deposit.amount) processed_amount += deposit.amount # Regardless of how the deposit was handled, we move on in the queue. diff --git a/specs/electra/light-client/fork.md b/specs/electra/light-client/fork.md index d613df56a9..ce3014abdd 100644 --- a/specs/electra/light-client/fork.md +++ b/specs/electra/light-client/fork.md @@ -61,7 +61,8 @@ def upgrade_lc_header_to_electra(pre: deneb.LightClientHeader) -> LightClientHea withdrawal_requests_root=Root(), # [New in Electra:EIP7002:EIP7251] consolidation_requests_root=Root(), # [New in Electra:EIP7251] ), - execution_branch=pre.execution_branch, + execution_branch=normalize_merkle_branch( + pre.execution_branch, EXECUTION_PAYLOAD_GINDEX_ELECTRA), ) ``` diff --git a/specs/electra/light-client/full-node.md b/specs/electra/light-client/full-node.md index f08a2cc5ed..36c1c54831 100644 --- a/specs/electra/light-client/full-node.md +++ b/specs/electra/light-client/full-node.md @@ -57,7 +57,7 @@ def block_to_light_client_header(block: SignedBeaconBlock) -> LightClientHeader: execution_header.consolidation_requests_root = hash_tree_root(payload.consolidation_requests) execution_branch = ExecutionBranch( - compute_merkle_proof(block.message.body, EXECUTION_PAYLOAD_GINDEX)) + compute_merkle_proof(block.message.body, execution_payload_gindex_at_slot(block.message.slot))) else: # Note that during fork transitions, `finalized_header` may still point to earlier forks. # While Bellatrix blocks also contain an `ExecutionPayload` (minus `withdrawals_root`), diff --git a/specs/electra/light-client/sync-protocol.md b/specs/electra/light-client/sync-protocol.md index ef9dcd5987..292075bc86 100644 --- a/specs/electra/light-client/sync-protocol.md +++ b/specs/electra/light-client/sync-protocol.md @@ -17,6 +17,7 @@ - [Modified `finalized_root_gindex_at_slot`](#modified-finalized_root_gindex_at_slot) - [Modified `current_sync_committee_gindex_at_slot`](#modified-current_sync_committee_gindex_at_slot) - [Modified `next_sync_committee_gindex_at_slot`](#modified-next_sync_committee_gindex_at_slot) + - [Modified `execution_payload_gindex_at_slot`](#modified-execution_payload_gindex_at_slot) - [Modified `get_lc_execution_root`](#modified-get_lc_execution_root) - [Modified `is_valid_light_client_header`](#modified-is_valid_light_client_header) @@ -38,26 +39,29 @@ Additional documents describes the impact of the upgrade on certain roles: | `FinalityBranch` | `Vector[Bytes32, floorlog2(FINALIZED_ROOT_GINDEX_ELECTRA)]` | Merkle branch of `finalized_checkpoint.root` within `BeaconState` | | `CurrentSyncCommitteeBranch` | `Vector[Bytes32, floorlog2(CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA)]` | Merkle branch of `current_sync_committee` within `BeaconState` | | `NextSyncCommitteeBranch` | `Vector[Bytes32, floorlog2(NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA)]` | Merkle branch of `next_sync_committee` within `BeaconState` | +| `ExecutionBranch` | `Vector[Bytes32, floorlog2(EXECUTION_PAYLOAD_GINDEX_ELECTRA)]` | Merkle branch of `execution_payload` within `BeaconBlockBody` | ## Constants ### Frozen constants -Existing `GeneralizedIndex` constants are frozen at their [Altair](../../altair/light-client/sync-protocol.md#constants) values. +Existing `GeneralizedIndex` constants are frozen at their [Altair](../../altair/light-client/sync-protocol.md#constants) and [Capella](../../capella/light-client/sync-protocol.md#constants) values. | Name | Value | | - | - | | `FINALIZED_ROOT_GINDEX` | `get_generalized_index(altair.BeaconState, 'finalized_checkpoint', 'root')` (= 105) | | `CURRENT_SYNC_COMMITTEE_GINDEX` | `get_generalized_index(altair.BeaconState, 'current_sync_committee')` (= 54) | | `NEXT_SYNC_COMMITTEE_GINDEX` | `get_generalized_index(altair.BeaconState, 'next_sync_committee')` (= 55) | +| `EXECUTION_PAYLOAD_GINDEX` | `get_generalized_index(capella.BeaconBlockBody, 'execution_payload')` (= 25) | ### New constants | Name | Value | | - | - | -| `FINALIZED_ROOT_GINDEX_ELECTRA` | `get_generalized_index(BeaconState, 'finalized_checkpoint', 'root')` (= 169) | -| `CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA` | `get_generalized_index(BeaconState, 'current_sync_committee')` (= 86) | -| `NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA` | `get_generalized_index(BeaconState, 'next_sync_committee')` (= 87) | +| `FINALIZED_ROOT_GINDEX_ELECTRA` | `get_generalized_index(BeaconState, 'finalized_checkpoint', 'root')` (= 553) | +| `CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA` | `get_generalized_index(BeaconState, 'current_sync_committee')` (= 278) | +| `NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA` | `get_generalized_index(BeaconState, 'next_sync_committee')` (= 279) | +| `EXECUTION_PAYLOAD_GINDEX_ELECTRA` | `get_generalized_index(BeaconBlockBody, 'execution_payload')` (= 137) | ## Helper functions @@ -97,6 +101,19 @@ def next_sync_committee_gindex_at_slot(slot: Slot) -> GeneralizedIndex: return NEXT_SYNC_COMMITTEE_GINDEX ``` +### Modified `execution_payload_gindex_at_slot` + +```python +def execution_payload_gindex_at_slot(slot: Slot) -> GeneralizedIndex: + epoch = compute_epoch_at_slot(slot) + assert epoch >= CAPELLA_FORK_EPOCH + + # [Modified in Electra] + if epoch >= ELECTRA_FORK_EPOCH: + return EXECUTION_PAYLOAD_GINDEX_ELECTRA + return EXECUTION_PAYLOAD_GINDEX +``` + ### Modified `get_lc_execution_root` ```python @@ -178,11 +195,10 @@ def is_valid_light_client_header(header: LightClientHeader) -> bool: and header.execution_branch == ExecutionBranch() ) - return is_valid_merkle_branch( + return is_valid_normalized_merkle_branch( leaf=get_lc_execution_root(header), branch=header.execution_branch, - depth=floorlog2(EXECUTION_PAYLOAD_GINDEX), - index=get_subtree_index(EXECUTION_PAYLOAD_GINDEX), + gindex=execution_payload_gindex_at_slot(header.beacon.slot), root=header.beacon.body_root, ) ``` diff --git a/specs/electra/p2p-interface.md b/specs/electra/p2p-interface.md index ebdcaaa831..ebd0e59f60 100644 --- a/specs/electra/p2p-interface.md +++ b/specs/electra/p2p-interface.md @@ -11,6 +11,8 @@ The specification of these changes continues in the same format as the network s - [Modifications in Electra](#modifications-in-electra) + - [Preset](#preset) + - [Custom types](#custom-types) - [The gossip domain: gossipsub](#the-gossip-domain-gossipsub) - [Topics and messages](#topics-and-messages) - [Global topics](#global-topics) @@ -22,6 +24,26 @@ The specification of these changes continues in the same format as the network s ## Modifications in Electra +### Preset + +Existing `PROOF_DEPTH` presets are frozen at their [Deneb](../../deneb/p2p-interface.md#preset) values. + +| Name | Value | +|------------------------------------------|-----------------------------------| +| `KZG_COMMITMENT_INCLUSION_PROOF_DEPTH` | `uint64(floorlog2(get_generalized_index(deneb.BeaconBlockBody, 'blob_kzg_commitments')) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK))` (= 17) | + +*[New in Electra:EIP7688]* + +| Name | Value | Description | +|------------------------------------------|-----------------------------------|---------------------------------------------------------------------| +| `KZG_COMMITMENT_INCLUSION_PROOF_DEPTH_ELECTRA` | `uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK))` (= 20) | Merkle proof depth for `blob_kzg_commitments` list item | + +### Custom types + +| Name | SSZ equivalent | Description | +| - | - | - | +| `KZGCommitmentInclusionProof` | `Vector[Bytes32, KZG_COMMITMENT_INCLUSION_PROOF_DEPTH_ELECTRA]` | Merkle branch of a single `blob_kzg_commitments` list item within `BeaconBlockBody` | + ### The gossip domain: gossipsub Some gossip meshes are upgraded in the fork of Electra to support upgraded types. diff --git a/tests/core/pyspec/eth2spec/test/capella/light_client/test_single_merkle_proof.py b/tests/core/pyspec/eth2spec/test/capella/light_client/test_single_merkle_proof.py index 7f414ab285..cb69f3fdee 100644 --- a/tests/core/pyspec/eth2spec/test/capella/light_client/test_single_merkle_proof.py +++ b/tests/core/pyspec/eth2spec/test/capella/light_client/test_single_merkle_proof.py @@ -6,6 +6,9 @@ from eth2spec.test.helpers.attestations import ( state_transition_with_full_block, ) +from eth2spec.test.helpers.light_client import ( + latest_execution_payload_gindex, +) @with_test_suite_name("BeaconBlockBody") @@ -15,7 +18,7 @@ def test_execution_merkle_proof(spec, state): block = state_transition_with_full_block(spec, state, True, False) yield "object", block.message.body - gindex = spec.EXECUTION_PAYLOAD_GINDEX + gindex = latest_execution_payload_gindex(spec) branch = spec.compute_merkle_proof(block.message.body, gindex) yield "proof", { "leaf": "0x" + block.message.body.execution_payload.hash_tree_root().hex(), diff --git a/tests/core/pyspec/eth2spec/test/deneb/unittests/test_config_invariants.py b/tests/core/pyspec/eth2spec/test/deneb/unittests/test_config_invariants.py index 1f44257856..691ff9aa9f 100644 --- a/tests/core/pyspec/eth2spec/test/deneb/unittests/test_config_invariants.py +++ b/tests/core/pyspec/eth2spec/test/deneb/unittests/test_config_invariants.py @@ -5,6 +5,12 @@ ) +def latest_kzg_commitment_inclusion_proof_depth(spec): + if hasattr(spec, 'KZG_COMMITMENT_INCLUSION_PROOF_DEPTH_ELECTRA'): + return spec.KZG_COMMITMENT_INCLUSION_PROOF_DEPTH_ELECTRA + return spec.KZG_COMMITMENT_INCLUSION_PROOF_DEPTH + + @with_deneb_and_later @spec_test @single_phase @@ -25,4 +31,4 @@ def test_networking(spec): assert spec.config.BLOB_SIDECAR_SUBNET_COUNT == spec.config.MAX_BLOBS_PER_BLOCK for i in range(spec.MAX_BLOB_COMMITMENTS_PER_BLOCK): gindex = spec.get_generalized_index(spec.BeaconBlockBody, 'blob_kzg_commitments', i) - assert spec.floorlog2(gindex) == spec.KZG_COMMITMENT_INCLUSION_PROOF_DEPTH + assert spec.floorlog2(gindex) == latest_kzg_commitment_inclusion_proof_depth(spec) diff --git a/tests/core/pyspec/eth2spec/test/helpers/light_client.py b/tests/core/pyspec/eth2spec/test/helpers/light_client.py index 4638c988b5..296f5ee160 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/light_client.py +++ b/tests/core/pyspec/eth2spec/test/helpers/light_client.py @@ -32,6 +32,12 @@ def latest_next_sync_committee_gindex(spec): return spec.NEXT_SYNC_COMMITTEE_GINDEX +def latest_execution_payload_gindex(spec): + if hasattr(spec, 'EXECUTION_PAYLOAD_GINDEX_ELECTRA'): + return spec.EXECUTION_PAYLOAD_GINDEX_ELECTRA + return spec.EXECUTION_PAYLOAD_GINDEX + + def compute_start_slot_at_sync_committee_period(spec, sync_committee_period): return spec.compute_start_slot_at_epoch(sync_committee_period * spec.EPOCHS_PER_SYNC_COMMITTEE_PERIOD) diff --git a/tests/core/pyspec/eth2spec/utils/ssz/ssz_typing.py b/tests/core/pyspec/eth2spec/utils/ssz/ssz_typing.py index 054c20e034..c10487ddfb 100644 --- a/tests/core/pyspec/eth2spec/utils/ssz/ssz_typing.py +++ b/tests/core/pyspec/eth2spec/utils/ssz/ssz_typing.py @@ -2,6 +2,7 @@ # Ignore linter: This module makes importing SSZ types easy, and hides away the underlying library from the spec. from remerkleable.complex import Container, Vector, List +from remerkleable.stable_container import StableContainer, Profile from remerkleable.union import Union from remerkleable.basic import boolean, bit, uint, byte, uint8, uint16, uint32, uint64, uint128, uint256 from remerkleable.bitfields import Bitvector, Bitlist