From e6e08e89869cc4b79b03c576b9e3d7e161bca724 Mon Sep 17 00:00:00 2001 From: Etan Kissling Date: Mon, 12 Dec 2022 00:43:07 +0100 Subject: [PATCH] Add `ExecutionPayloadHeader` to LC data While the light client sync protocol currently provides access to the latest `BeaconBlockHeader`, obtaining the matching execution data needs workarounds such as downloading the full block. Having ready access to the EL state root simplifies use cases that need a way to cross-check `eth_getProof` responses against LC data. Access to `block_hash` unlocks scenarios where a CL light client drives an EL without `engine_newPayload`. As of Altair, only the CL beacon block root is available, but the EL block hash is needed for engine API. Other fields in the `ExecutionPayloadHeader` such as `logs_bloom` may allow light client applications to monitor blocks for local interest, e.g. for transfers affecting a certain wallet. This enables to download only the few relevant blocks instead of every single one. A new `LightClientStore` is proposed into the Capella spec that may be used to sync LC data that includes execution data. Existing pre-Capella LC data will remain as is, but can be locally upgraded before feeding it into the new `LightClientStore` so that light clients do not need to run a potentially expensive fork transition at a specific time. This enables the `LightClientStore` to be upgraded at a use case dependent timing at any time before Capella hits. Smart contract and embedded deployments benefit from reduced code size and do not need synchronization with the beacon chain clock to perform the Capella fork. --- Makefile | 2 +- README.md | 2 +- setup.py | 20 + specs/capella/light-client/fork.md | 93 +++++ specs/capella/light-client/full-node.md | 72 ++++ specs/capella/light-client/p2p-interface.md | 99 +++++ specs/capella/light-client/sync-protocol.md | 289 +++++++++++++ .../test/altair/light_client/test_sync.py | 392 +++++++++++++++--- .../test/capella/light_client/__init__.py | 0 .../light_client/test_single_merkle_proof.py | 31 ++ tests/formats/light_client/sync.md | 25 +- tests/generators/light_client/main.py | 8 +- 12 files changed, 974 insertions(+), 59 deletions(-) create mode 100644 specs/capella/light-client/fork.md create mode 100644 specs/capella/light-client/full-node.md create mode 100644 specs/capella/light-client/p2p-interface.md create mode 100644 specs/capella/light-client/sync-protocol.md create mode 100644 tests/core/pyspec/eth2spec/test/capella/light_client/__init__.py create mode 100644 tests/core/pyspec/eth2spec/test/capella/light_client/test_single_merkle_proof.py diff --git a/Makefile b/Makefile index 73450562b5..728656cb6a 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ GENERATOR_VENVS = $(patsubst $(GENERATOR_DIR)/%, $(GENERATOR_DIR)/%venv, $(GENER MARKDOWN_FILES = $(wildcard $(SPEC_DIR)/phase0/*.md) \ $(wildcard $(SPEC_DIR)/altair/*.md) $(wildcard $(SPEC_DIR)/altair/**/*.md) \ $(wildcard $(SPEC_DIR)/bellatrix/*.md) \ - $(wildcard $(SPEC_DIR)/capella/*.md) \ + $(wildcard $(SPEC_DIR)/capella/*.md) $(wildcard $(SPEC_DIR)/capella/**/*.md) \ $(wildcard $(SPEC_DIR)/custody/*.md) \ $(wildcard $(SPEC_DIR)/das/*.md) \ $(wildcard $(SPEC_DIR)/sharding/*.md) \ diff --git a/README.md b/README.md index 07b78c0d58..766b6eda7b 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Features are researched and developed in parallel, and then consolidated into se ### In-development Specifications | Code Name or Topic | Specs | Notes | | - | - | - | -| Capella (tentative) | | +| Capella (tentative) | | | EIP4844 (tentative) | | | Sharding (outdated) | | | Custody Game (outdated) | | Dependent on sharding | diff --git a/setup.py b/setup.py index 432a41fe4c..db0ad0cfc4 100644 --- a/setup.py +++ b/setup.py @@ -616,6 +616,21 @@ def imports(cls, preset_name: str): ''' + @classmethod + def sundry_functions(cls) -> str: + return super().sundry_functions() + '\n\n' + ''' +def compute_merkle_proof_for_block_body(body: BeaconBlockBody, + index: GeneralizedIndex) -> Sequence[Bytes32]: + return build_proof(body.get_backing(), index)''' + + + @classmethod + def hardcoded_ssz_dep_constants(cls) -> Dict[str, str]: + constants = { + 'EXECUTION_PAYLOAD_INDEX': 'GeneralizedIndex(25)', + } + return {**super().hardcoded_ssz_dep_constants(), **constants} + # # EIP4844SpecBuilder # @@ -718,6 +733,7 @@ def format_protocol(protocol_name: str, protocol_def: ProtocolDefinition) -> str if k in [ "ceillog2", "floorlog2", + "compute_merkle_proof_for_block_body", "compute_merkle_proof_for_state", ]: del spec_object.functions[k] @@ -1010,6 +1026,10 @@ def finalize_options(self): """ if self.spec_fork in (CAPELLA, EIP4844): self.md_doc_paths += """ + specs/capella/light-client/fork.md + specs/capella/light-client/full-node.md + specs/capella/light-client/p2p-interface.md + specs/capella/light-client/sync-protocol.md specs/capella/beacon-chain.md specs/capella/fork.md specs/capella/fork-choice.md diff --git a/specs/capella/light-client/fork.md b/specs/capella/light-client/fork.md new file mode 100644 index 0000000000..700f736c27 --- /dev/null +++ b/specs/capella/light-client/fork.md @@ -0,0 +1,93 @@ +# Capella Light Client -- Fork Logic + +## Table of contents + + + + + +- [Introduction](#introduction) + - [Upgrading light client data](#upgrading-light-client-data) + - [Upgrading the store](#upgrading-the-store) + + + + +## Introduction + +This document describes how to upgrade existing light client objects based on the [Altair specification](../../altair/light-client/sync-protocol.md) to Capella. This is necessary when processing pre-Capella data with a post-Capella `LightClientStore`. Note that the data being exchanged over the network protocols uses the original format. + +### Upgrading light client data + +A Capella `LightClientStore` can still process earlier light client data. In order to do so, that pre-Capella data needs to be locally upgraded to Capella before processing. + +```python +def upgrade_lc_header_to_capella(pre: BeaconBlockHeader) -> LightClientHeader: + return LightClientHeader( + beacon=pre, + execution=ExecutionPayloadHeader(), + ) +``` + +```python +def upgrade_lc_bootstrap_to_capella(pre: altair.LightClientBootstrap) -> LightClientBootstrap: + return LightClientBootstrap( + header=upgrade_lc_header_to_capella(pre.header), + current_sync_committee=pre.current_sync_committee, + current_sync_committee_branch=pre.current_sync_committee_branch, + ) +``` + +```python +def upgrade_lc_update_to_capella(pre: altair.LightClientUpdate) -> LightClientUpdate: + return LightClientUpdate( + attested_header=upgrade_lc_header_to_capella(pre.attested_header), + next_sync_committee=pre.next_sync_committee, + next_sync_committee_branch=pre.next_sync_committee_branch, + finalized_header=upgrade_lc_header_to_capella(pre.finalized_header), + finality_branch=pre.finality_branch, + sync_aggregate=pre.sync_aggregate, + signature_slot=pre.signature_slot, + ) +``` + +```python +def upgrade_lc_finality_update_to_capella(pre: altair.LightClientFinalityUpdate) -> LightClientFinalityUpdate: + return LightClientFinalityUpdate( + attested_header=upgrade_lc_header_to_capella(pre.attested_header), + finalized_header=upgrade_lc_header_to_capella(pre.finalized_header), + finality_branch=pre.finality_branch, + sync_aggregate=pre.sync_aggregate, + signature_slot=pre.signature_slot, + ) +``` + +```python +def upgrade_lc_optimistic_update_to_capella(pre: altair.LightClientOptimisticUpdate) -> LightClientOptimisticUpdate: + return LightClientOptimisticUpdate( + attested_header=upgrade_lc_header_to_capella(pre.attested_header), + sync_aggregate=pre.sync_aggregate, + signature_slot=pre.signature_slot, + ) +``` + +### Upgrading the store + +Existing `LightClientStore` objects based on Altair MUST be upgraded to Capella before Capella based light client data can be processed. The `LightClientStore` upgrade MAY be performed before `CAPELLA_FORK_EPOCH`. + +```python +def upgrade_lc_store_to_capella(pre: altair.LightClientStore) -> LightClientStore: + if pre.best_valid_update is None: + best_valid_update = None + else: + best_valid_update = upgrade_lc_update_to_capella(pre.best_valid_update) + return LightClientStore( + finalized_header=upgrade_lc_header_to_capella(pre.finalized_header), + current_sync_committee=pre.current_sync_committee, + next_sync_committee=pre.next_sync_committee, + best_valid_update=best_valid_update, + optimistic_header=upgrade_lc_header_to_capella(pre.optimistic_header), + previous_max_active_participants=pre.previous_max_active_participants, + current_max_active_participants=pre.current_max_active_participants, + ) +``` diff --git a/specs/capella/light-client/full-node.md b/specs/capella/light-client/full-node.md new file mode 100644 index 0000000000..104853f8e5 --- /dev/null +++ b/specs/capella/light-client/full-node.md @@ -0,0 +1,72 @@ +# Capella Light Client -- Full Node + +**Notice**: This document is a work-in-progress for researchers and implementers. + +## Table of contents + + + + + +- [Introduction](#introduction) +- [Helper functions](#helper-functions) + - [`compute_merkle_proof_for_block_body`](#compute_merkle_proof_for_block_body) + - [Modified `block_to_light_client_header`](#modified-block_to_light_client_header) + + + + +## Introduction + +This upgrade adds information about the execution payload to light client data as part of the Capella upgrade. + +## Helper functions + +### `compute_merkle_proof_for_block_body` + +```python +def compute_merkle_proof_for_block_body(body: BeaconBlockBody, + index: GeneralizedIndex) -> Sequence[Bytes32]: + ... +``` + +### Modified `block_to_light_client_header` + +```python +def block_to_light_client_header(block: SignedBeaconBlock) -> LightClientHeader: + if compute_epoch_at_slot(block.message.slot) >= CAPELLA_FORK_EPOCH: + payload = block.message.body.execution_payload + execution_header = ExecutionPayloadHeader( + parent_hash=payload.parent_hash, + fee_recipient=payload.fee_recipient, + state_root=payload.state_root, + receipts_root=payload.receipts_root, + logs_bloom=payload.logs_bloom, + prev_randao=payload.prev_randao, + block_number=payload.block_number, + gas_limit=payload.gas_limit, + gas_used=payload.gas_used, + timestamp=payload.timestamp, + extra_data=payload.extra_data, + base_fee_per_gas=payload.base_fee_per_gas, + block_hash=payload.block_hash, + transactions_root=hash_tree_root(payload.transactions), + withdrawals_root=hash_tree_root(payload.withdrawals), + ) + execution_branch = compute_merkle_proof_for_block_body(block.message.body, EXECUTION_PAYLOAD_INDEX) + else: + execution_header = ExecutionPayloadHeader() + execution_branch = [Bytes32() for _ in range(floorlog2(EXECUTION_PAYLOAD_INDEX))] + + return LightClientHeader( + beacon=BeaconBlockHeader( + slot=block.message.slot, + proposer_index=block.message.proposer_index, + parent_root=block.message.parent_root, + state_root=block.message.state_root, + body_root=hash_tree_root(block.message.body), + ), + execution=execution_header, + execution_branch=execution_branch, + ) +``` diff --git a/specs/capella/light-client/p2p-interface.md b/specs/capella/light-client/p2p-interface.md new file mode 100644 index 0000000000..b6c1ec0808 --- /dev/null +++ b/specs/capella/light-client/p2p-interface.md @@ -0,0 +1,99 @@ +# Capella Light Client -- Networking + +**Notice**: This document is a work-in-progress for researchers and implementers. + +## Table of contents + + + + + +- [Networking](#networking) + - [The gossip domain: gossipsub](#the-gossip-domain-gossipsub) + - [Topics and messages](#topics-and-messages) + - [Global topics](#global-topics) + - [`light_client_finality_update`](#light_client_finality_update) + - [`light_client_optimistic_update`](#light_client_optimistic_update) + - [The Req/Resp domain](#the-reqresp-domain) + - [Messages](#messages) + - [GetLightClientBootstrap](#getlightclientbootstrap) + - [LightClientUpdatesByRange](#lightclientupdatesbyrange) + - [GetLightClientFinalityUpdate](#getlightclientfinalityupdate) + - [GetLightClientOptimisticUpdate](#getlightclientoptimisticupdate) + + + + +## Networking + +The [Altair light client networking specification](../../altair/light-client/p2p-interface.md) is extended to exchange [Capella light client data](./sync-protocol.md). + +### The gossip domain: gossipsub + +#### Topics and messages + +##### Global topics + +###### `light_client_finality_update` + +[0]: # (eth2spec: skip) + +| `fork_version` | Message SSZ type | +| ------------------------------------------------------ | ------------------------------------- | +| `GENESIS_FORK_VERSION` | n/a | +| `ALTAIR_FORK_VERSION` through `BELLATRIX_FORK_VERSION` | `altair.LightClientFinalityUpdate` | +| `CAPELLA_FORK_VERSION` and later | `capella.LightClientFinalityUpdate` | + +###### `light_client_optimistic_update` + +[0]: # (eth2spec: skip) + +| `fork_version` | Message SSZ type | +| ------------------------------------------------------ | ------------------------------------- | +| `GENESIS_FORK_VERSION` | n/a | +| `ALTAIR_FORK_VERSION` through `BELLATRIX_FORK_VERSION` | `altair.LightClientOptimisticUpdate` | +| `CAPELLA_FORK_VERSION` and later | `capella.LightClientOptimisticUpdate` | + +### The Req/Resp domain + +#### Messages + +##### GetLightClientBootstrap + +[0]: # (eth2spec: skip) + +| `fork_version` | Response SSZ type | +| ------------------------------------------------------ | ------------------------------------- | +| `GENESIS_FORK_VERSION` | n/a | +| `ALTAIR_FORK_VERSION` through `BELLATRIX_FORK_VERSION` | `altair.LightClientBootstrap` | +| `CAPELLA_FORK_VERSION` and later | `capella.LightClientBootstrap` | + +##### LightClientUpdatesByRange + +[0]: # (eth2spec: skip) + +| `fork_version` | Response chunk SSZ type | +| ------------------------------------------------------ | ------------------------------------- | +| `GENESIS_FORK_VERSION` | n/a | +| `ALTAIR_FORK_VERSION` through `BELLATRIX_FORK_VERSION` | `altair.LightClientUpdate` | +| `CAPELLA_FORK_VERSION` and later | `capella.LightClientUpdate` | + +##### GetLightClientFinalityUpdate + +[0]: # (eth2spec: skip) + +| `fork_version` | Response SSZ type | +| ------------------------------------------------------ | ------------------------------------- | +| `GENESIS_FORK_VERSION` | n/a | +| `ALTAIR_FORK_VERSION` through `BELLATRIX_FORK_VERSION` | `altair.LightClientFinalityUpdate` | +| `CAPELLA_FORK_VERSION` and later | `capella.LightClientFinalityUpdate` | + +##### GetLightClientOptimisticUpdate + +[0]: # (eth2spec: skip) + +| `fork_version` | Response SSZ type | +| ------------------------------------------------------ | ------------------------------------- | +| `GENESIS_FORK_VERSION` | n/a | +| `ALTAIR_FORK_VERSION` through `BELLATRIX_FORK_VERSION` | `altair.LightClientOptimisticUpdate` | +| `CAPELLA_FORK_VERSION` and later | `capella.LightClientOptimisticUpdate` | diff --git a/specs/capella/light-client/sync-protocol.md b/specs/capella/light-client/sync-protocol.md new file mode 100644 index 0000000000..fe64c11e43 --- /dev/null +++ b/specs/capella/light-client/sync-protocol.md @@ -0,0 +1,289 @@ +# Capella Light Client -- Sync Protocol + +**Notice**: This document is a work-in-progress for researchers and implementers. + +## Table of contents + + + + + +- [Introduction](#introduction) +- [Constants](#constants) +- [Containers](#containers) + - [`LightClientHeader`](#lightclientheader) + - [Modified `LightClientBootstrap`](#modified-lightclientbootstrap) + - [Modified `LightClientUpdate`](#modified-lightclientupdate) + - [Modified `LightClientFinalityUpdate`](#modified-lightclientfinalityupdate) + - [Modified `LightClientOptimisticUpdate`](#modified-lightclientoptimisticupdate) + - [Modified `LightClientStore`](#modified-lightclientstore) +- [Helper functions](#helper-functions) + - [Modified `get_lc_beacon_slot`](#modified-get_lc_beacon_slot) + - [Modified `get_lc_beacon_root`](#modified-get_lc_beacon_root) + - [`get_lc_execution_root`](#get_lc_execution_root) + - [`is_valid_light_client_header`](#is_valid_light_client_header) +- [Light client initialization](#light-client-initialization) + - [Modified `initialize_light_client_store`](#modified-initialize_light_client_store) +- [Light client state updates](#light-client-state-updates) + - [Modified `validate_light_client_update`](#modified-validate_light_client_update) + + + + +## Introduction + +This upgrade adds information about the execution payload to light client data as part of the Capella upgrade. It extends the [Altair Light Client specifications](../../altair/light-client/sync-protocol.md). The [fork document](./fork.md) explains how to upgrade existing Altair based deployments to Capella. + +Additional documents describes the impact of the upgrade on certain roles: +- [Full node](./full-node.md) +- [Networking](./p2p-interface.md) + +## Constants + +| Name | Value | +| - | - | +| `EXECUTION_PAYLOAD_INDEX` | `get_generalized_index(BeaconBlockBody, 'execution_payload')` (= 25) | + +## Containers + +### `LightClientHeader` + +```python +class LightClientHeader(Container): + # Beacon block header + beacon: BeaconBlockHeader + # Execution payload header corresponding to `beacon.body_root` (from Capella onward) + execution: ExecutionPayloadHeader + execution_branch: Vector[Bytes32, floorlog2(EXECUTION_PAYLOAD_INDEX)] +``` + +### Modified `LightClientBootstrap` + +```python +class LightClientBootstrap(Container): + # Header matching the requested beacon block root + header: LightClientHeader # [Modified in Capella] + # Current sync committee corresponding to `header.beacon.state_root` + current_sync_committee: SyncCommittee + current_sync_committee_branch: Vector[Bytes32, floorlog2(CURRENT_SYNC_COMMITTEE_INDEX)] +``` + +### Modified `LightClientUpdate` + +```python +class LightClientUpdate(Container): + # Header attested to by the sync committee + attested_header: LightClientHeader # [Modified in Capella] + # Next sync committee corresponding to `attested_header.beacon.state_root` + next_sync_committee: SyncCommittee + next_sync_committee_branch: Vector[Bytes32, floorlog2(NEXT_SYNC_COMMITTEE_INDEX)] + # Finalized header corresponding to `attested_header.beacon.state_root` + finalized_header: LightClientHeader # [Modified in Capella] + finality_branch: Vector[Bytes32, floorlog2(FINALIZED_ROOT_INDEX)] + # Sync committee aggregate signature + sync_aggregate: SyncAggregate + # Slot at which the aggregate signature was created (untrusted) + signature_slot: Slot +``` + +### Modified `LightClientFinalityUpdate` + +```python +class LightClientFinalityUpdate(Container): + # Header attested to by the sync committee + attested_header: LightClientHeader # [Modified in Capella] + # Finalized header corresponding to `attested_header.beaon.state_root` + finalized_header: LightClientHeader # [Modified in Capella] + finality_branch: Vector[Bytes32, floorlog2(FINALIZED_ROOT_INDEX)] + # Sync committee aggregate signature + sync_aggregate: SyncAggregate + # Slot at which the aggregate signature was created (untrusted) + signature_slot: Slot +``` + +### Modified `LightClientOptimisticUpdate` + +```python +class LightClientOptimisticUpdate(Container): + # Header attested to by the sync committee + attested_header: LightClientHeader # [Modified in Capella] + # Sync committee aggregate signature + sync_aggregate: SyncAggregate + # Slot at which the aggregate signature was created (untrusted) + signature_slot: Slot +``` + +### Modified `LightClientStore` + +```python +@dataclass +class LightClientStore(object): + # Header that is finalized + finalized_header: LightClientHeader # [Modified in Capella] + # Sync committees corresponding to the finalized header + current_sync_committee: SyncCommittee + next_sync_committee: SyncCommittee + # Best available header to switch finalized head to if we see nothing else + best_valid_update: Optional[LightClientUpdate] + # Most recent available reasonably-safe header + optimistic_header: LightClientHeader # [Modified in Capella] + # Max number of active participants in a sync committee (used to calculate safety threshold) + previous_max_active_participants: uint64 + current_max_active_participants: uint64 +``` + +## Helper functions + +### Modified `get_lc_beacon_slot` + +```python +def get_lc_beacon_slot(header: LightClientHeader) -> Slot: + return header.beacon.slot +``` + +### Modified `get_lc_beacon_root` + +```python +def get_lc_beacon_root(header: LightClientHeader) -> Root: + return hash_tree_root(header.beacon) +``` + +### `get_lc_execution_root` + +```python +def get_lc_execution_root(header: LightClientHeader) -> Root: + if compute_epoch_at_slot(get_lc_beacon_slot(header)) >= CAPELLA_FORK_EPOCH: + return hash_tree_root(header.execution) + + return Root() +``` + +### `is_valid_light_client_header` + +```python +def is_valid_light_client_header(header: LightClientHeader) -> bool: + if compute_epoch_at_slot(get_lc_beacon_slot(header)) >= CAPELLA_FORK_EPOCH: + return is_valid_merkle_branch( + leaf=get_lc_execution_root(header), + branch=header.execution_branch, + depth=floorlog2(EXECUTION_PAYLOAD_INDEX), + index=get_subtree_index(EXECUTION_PAYLOAD_INDEX), + root=header.beacon.body_root, + ) + + return ( + header.execution == ExecutionPayloadHeader() + and header.execution_branch == [Bytes32() for _ in range(floorlog2(EXECUTION_PAYLOAD_INDEX))] + ) +``` + +## Light client initialization + +### Modified `initialize_light_client_store` + +```python +def initialize_light_client_store(trusted_block_root: Root, + bootstrap: LightClientBootstrap) -> LightClientStore: + assert is_valid_light_client_header(bootstrap.header) # [New in Capella] + assert get_lc_beacon_root(bootstrap.header) == trusted_block_root + + assert is_valid_merkle_branch( + leaf=hash_tree_root(bootstrap.current_sync_committee), + branch=bootstrap.current_sync_committee_branch, + depth=floorlog2(CURRENT_SYNC_COMMITTEE_INDEX), + index=get_subtree_index(CURRENT_SYNC_COMMITTEE_INDEX), + root=bootstrap.header.beacon.state_root, # [Modified in Capella] + ) + + return LightClientStore( + finalized_header=bootstrap.header, + current_sync_committee=bootstrap.current_sync_committee, + next_sync_committee=SyncCommittee(), + best_valid_update=None, + optimistic_header=bootstrap.header, + previous_max_active_participants=0, + current_max_active_participants=0, + ) +``` + +## Light client state updates + +### Modified `validate_light_client_update` + +```python +def validate_light_client_update(store: LightClientStore, + update: LightClientUpdate, + current_slot: Slot, + genesis_validators_root: Root) -> None: + # Verify sync committee has sufficient participants + sync_aggregate = update.sync_aggregate + assert sum(sync_aggregate.sync_committee_bits) >= MIN_SYNC_COMMITTEE_PARTICIPANTS + + # Verify update does not skip a sync committee period + assert is_valid_light_client_header(update.attested_header) # [New in Capella] + update_attested_slot = get_lc_beacon_slot(update.attested_header) + update_finalized_slot = get_lc_beacon_slot(update.finalized_header) + assert current_slot >= update.signature_slot > update_attested_slot >= update_finalized_slot + store_period = compute_sync_committee_period_at_slot(get_lc_beacon_slot(store.finalized_header)) + update_signature_period = compute_sync_committee_period_at_slot(update.signature_slot) + if is_next_sync_committee_known(store): + assert update_signature_period in (store_period, store_period + 1) + else: + assert update_signature_period == store_period + + # Verify update is relevant + update_attested_period = compute_sync_committee_period_at_slot(update_attested_slot) + update_has_next_sync_committee = not is_next_sync_committee_known(store) and ( + is_sync_committee_update(update) and update_attested_period == store_period + ) + assert update_attested_slot > update_finalized_slot or update_has_next_sync_committee + + # Verify that the `finality_branch`, if present, confirms `finalized_header.beacon` + # to match the finalized checkpoint root saved in the state of `attested_header.beacon`. + # Note that the genesis finalized checkpoint root is represented as a zero hash. + if not is_finality_update(update): + assert update.finalized_header == LightClientHeader() # [Modified in Capella] + else: + if update_finalized_slot == GENESIS_SLOT: + assert update.finalized_header == LightClientHeader() # [Modified in Capella] + finalized_root = Bytes32() + else: + assert is_valid_light_client_header(update.finalized_header) # [New in Capella] + finalized_root = get_lc_beacon_root(update.finalized_header) + assert is_valid_merkle_branch( + leaf=finalized_root, + branch=update.finality_branch, + depth=floorlog2(FINALIZED_ROOT_INDEX), + index=get_subtree_index(FINALIZED_ROOT_INDEX), + root=update.attested_header.beacon.state_root, # [Modified in Capella] + ) + + # Verify that the `next_sync_committee`, if present, actually is the next sync committee saved in the + # state of the `attested_header.beacon` + if not is_sync_committee_update(update): + assert update.next_sync_committee == SyncCommittee() + else: + if update_attested_period == store_period and is_next_sync_committee_known(store): + assert update.next_sync_committee == store.next_sync_committee + assert is_valid_merkle_branch( + leaf=hash_tree_root(update.next_sync_committee), + branch=update.next_sync_committee_branch, + depth=floorlog2(NEXT_SYNC_COMMITTEE_INDEX), + index=get_subtree_index(NEXT_SYNC_COMMITTEE_INDEX), + root=update.attested_header.beacon.state_root, # [Modified in Capella] + ) + + # Verify sync committee aggregate signature + if update_signature_period == store_period: + sync_committee = store.current_sync_committee + else: + sync_committee = store.next_sync_committee + participant_pubkeys = [ + pubkey for (bit, pubkey) in zip(sync_aggregate.sync_committee_bits, sync_committee.pubkeys) + if bit + ] + fork_version = compute_fork_version(compute_epoch_at_slot(update.signature_slot)) + domain = compute_domain(DOMAIN_SYNC_COMMITTEE, fork_version, genesis_validators_root) + signing_root = compute_signing_root(update.attested_header.beacon, domain) # [Modified in Capella] + assert bls.FastAggregateVerify(participant_pubkeys, signing_root, sync_aggregate.sync_committee_signature) +``` diff --git a/tests/core/pyspec/eth2spec/test/altair/light_client/test_sync.py b/tests/core/pyspec/eth2spec/test/altair/light_client/test_sync.py index cc6a070a38..53215670ae 100644 --- a/tests/core/pyspec/eth2spec/test/altair/light_client/test_sync.py +++ b/tests/core/pyspec/eth2spec/test/altair/light_client/test_sync.py @@ -3,14 +3,30 @@ from eth_utils import encode_hex from eth2spec.test.context import ( spec_state_test_with_matching_config, + spec_test, + with_config_overrides, + with_matching_spec_config, + with_phases, with_presets, + with_state, with_altair_and_later, ) from eth2spec.test.helpers.attestations import ( next_slots_with_attestations, state_transition_with_full_block, ) -from eth2spec.test.helpers.constants import MINIMAL +from eth2spec.test.helpers.constants import ( + PHASE0, ALTAIR, BELLATRIX, CAPELLA, + MINIMAL, + ALL_PHASES, +) +from eth2spec.test.helpers.fork_transition import ( + do_fork, +) +from eth2spec.test.helpers.forks import ( + is_post_capella, + is_post_fork, +) from eth2spec.test.helpers.light_client import ( get_sync_aggregate, ) @@ -20,25 +36,117 @@ ) -def setup_test(spec, state): - class LightClientSyncTest(object): - steps: List[Dict[str, Any]] - genesis_validators_root: spec.Root - store: spec.LightClientStore +def get_spec_for_fork_version(spec, fork_version, phases): + if phases is None: + return spec + for fork in [fork for fork in ALL_PHASES if is_post_fork(spec.fork, fork)]: + if fork == PHASE0: + fork_version_field = 'GENESIS_FORK_VERSION' + else: + fork_version_field = fork.upper() + '_FORK_VERSION' + if fork_version == getattr(spec.config, fork_version_field): + return phases[fork] + raise ValueError("Unknown fork version %s" % fork_version) + + +def needs_upgrade_to_capella(d_spec, s_spec): + return is_post_capella(s_spec) and not is_post_capella(d_spec) + + +def upgrade_lc_bootstrap_to_store(d_spec, s_spec, data): + if not needs_upgrade_to_capella(d_spec, s_spec): + return data + + upgraded = s_spec.upgrade_lc_bootstrap_to_capella(data) + assert s_spec.get_lc_beacon_slot(upgraded.header) == d_spec.get_lc_beacon_slot(data.header) + assert s_spec.get_lc_beacon_root(upgraded.header) == d_spec.get_lc_beacon_root(data.header) + assert s_spec.get_lc_execution_root(upgraded.header) == s_spec.Root() + assert upgraded.current_sync_committee == data.current_sync_committee + assert upgraded.current_sync_committee_branch == data.current_sync_committee_branch + return upgraded + + +def upgrade_lc_update_to_store(d_spec, s_spec, data): + if not needs_upgrade_to_capella(d_spec, s_spec): + return data + + upgraded = s_spec.upgrade_lc_update_to_capella(data) + assert s_spec.get_lc_beacon_slot(upgraded.attested_header) == d_spec.get_lc_beacon_slot(data.attested_header) + assert s_spec.get_lc_beacon_root(upgraded.attested_header) == d_spec.get_lc_beacon_root(data.attested_header) + assert s_spec.get_lc_execution_root(upgraded.attested_header) == s_spec.Root() + assert upgraded.next_sync_committee == data.next_sync_committee + assert upgraded.next_sync_committee_branch == data.next_sync_committee_branch + assert s_spec.get_lc_beacon_slot(upgraded.finalized_header) == d_spec.get_lc_beacon_slot(data.finalized_header) + assert s_spec.get_lc_beacon_root(upgraded.finalized_header) == d_spec.get_lc_beacon_root(data.finalized_header) + assert s_spec.get_lc_execution_root(upgraded.finalized_header) == s_spec.Root() + assert upgraded.sync_aggregate == data.sync_aggregate + assert upgraded.signature_slot == data.signature_slot + return upgraded + + +def upgrade_lc_store_to_new_spec(d_spec, s_spec, data): + if not needs_upgrade_to_capella(d_spec, s_spec): + return data + + upgraded = s_spec.upgrade_lc_store_to_capella(data) + assert s_spec.get_lc_beacon_slot(upgraded.finalized_header) == d_spec.get_lc_beacon_slot(data.finalized_header) + assert s_spec.get_lc_beacon_root(upgraded.finalized_header) == d_spec.get_lc_beacon_root(data.finalized_header) + assert s_spec.get_lc_execution_root(upgraded.finalized_header) == s_spec.Root() + assert upgraded.current_sync_committee == data.current_sync_committee + assert upgraded.next_sync_committee == data.next_sync_committee + if upgraded.best_valid_update is None: + assert data.best_valid_update is None + else: + assert upgraded.best_valid_update == upgrade_lc_update_to_store(d_spec, s_spec, data.best_valid_update) + assert s_spec.get_lc_beacon_slot(upgraded.optimistic_header) == d_spec.get_lc_beacon_slot(data.optimistic_header) + assert s_spec.get_lc_beacon_root(upgraded.optimistic_header) == d_spec.get_lc_beacon_root(data.optimistic_header) + assert s_spec.get_lc_execution_root(upgraded.optimistic_header) == s_spec.Root() + assert upgraded.previous_max_active_participants == data.previous_max_active_participants + assert upgraded.current_max_active_participants == data.current_max_active_participants + return upgraded + + +class LightClientSyncTest(object): + steps: List[Dict[str, Any]] + genesis_validators_root: Any + s_spec: Any + store: Any + +def get_store_fork_version(s_spec): + if is_post_capella(s_spec): + return s_spec.config.CAPELLA_FORK_VERSION + return s_spec.config.ALTAIR_FORK_VERSION + + +def setup_test(spec, state, s_spec=None, phases=None): test = LightClientSyncTest() test.steps = [] + if s_spec is None: + s_spec = spec + test.s_spec = s_spec + yield "genesis_validators_root", "meta", "0x" + state.genesis_validators_root.hex() test.genesis_validators_root = state.genesis_validators_root next_slots(spec, state, spec.SLOTS_PER_EPOCH * 2 - 1) trusted_block = state_transition_with_full_block(spec, state, True, True) trusted_block_root = trusted_block.message.hash_tree_root() - bootstrap = spec.create_light_client_bootstrap(state, trusted_block) yield "trusted_block_root", "meta", "0x" + trusted_block_root.hex() - yield "bootstrap", bootstrap - test.store = spec.initialize_light_client_store(trusted_block_root, bootstrap) + + data_fork_version = spec.compute_fork_version(spec.compute_epoch_at_slot(trusted_block.message.slot)) + data_fork_digest = spec.compute_fork_digest(data_fork_version, test.genesis_validators_root) + d_spec = get_spec_for_fork_version(spec, data_fork_version, phases) + data = d_spec.create_light_client_bootstrap(state, trusted_block) + yield "bootstrap_fork_digest", "meta", encode_hex(data_fork_digest) + yield "bootstrap", data + + upgraded = upgrade_lc_bootstrap_to_store(d_spec, test.s_spec, data) + test.store = test.s_spec.initialize_light_client_store(trusted_block_root, upgraded) + store_fork_version = get_store_fork_version(test.s_spec) + store_fork_digest = test.s_spec.compute_fork_digest(store_fork_version, test.genesis_validators_root) + yield "store_fork_digest", "meta", encode_hex(store_fork_digest) return test @@ -47,62 +155,97 @@ def finish_test(test): yield "steps", test.steps -def get_update_file_name(spec, update): - if spec.is_sync_committee_update(update): +def get_update_file_name(d_spec, update): + if d_spec.is_sync_committee_update(update): suffix1 = "s" else: suffix1 = "x" - if spec.is_finality_update(update): + if d_spec.is_finality_update(update): suffix2 = "f" else: suffix2 = "x" - return f"update_{encode_hex(spec.get_lc_beacon_root(update.attested_header))}_{suffix1}{suffix2}" - + return f"update_{encode_hex(d_spec.get_lc_beacon_root(update.attested_header))}_{suffix1}{suffix2}" + + +def get_checks(s_spec, store): + if is_post_capella(s_spec): + return { + "finalized_header": { + 'slot': int(s_spec.get_lc_beacon_slot(store.finalized_header)), + 'beacon_root': encode_hex(s_spec.get_lc_beacon_root(store.finalized_header)), + 'execution_root': encode_hex(s_spec.get_lc_execution_root(store.finalized_header)), + }, + "optimistic_header": { + 'slot': int(s_spec.get_lc_beacon_slot(store.optimistic_header)), + 'beacon_root': encode_hex(s_spec.get_lc_beacon_root(store.optimistic_header)), + 'execution_root': encode_hex(s_spec.get_lc_execution_root(store.optimistic_header)), + }, + } -def get_checks(spec, store): return { "finalized_header": { - 'slot': int(spec.get_lc_beacon_slot(store.finalized_header)), - 'beacon_root': encode_hex(spec.get_lc_beacon_root(store.finalized_header)), + 'slot': int(s_spec.get_lc_beacon_slot(store.finalized_header)), + 'beacon_root': encode_hex(s_spec.get_lc_beacon_root(store.finalized_header)), }, "optimistic_header": { - 'slot': int(spec.get_lc_beacon_slot(store.optimistic_header)), - 'beacon_root': encode_hex(spec.get_lc_beacon_root(store.optimistic_header)), + 'slot': int(s_spec.get_lc_beacon_slot(store.optimistic_header)), + 'beacon_root': encode_hex(s_spec.get_lc_beacon_root(store.optimistic_header)), }, } def emit_force_update(test, spec, state): current_slot = state.slot - spec.process_light_client_store_force_update(test.store, current_slot) + test.s_spec.process_light_client_store_force_update(test.store, current_slot) yield from [] # Consistently enable `yield from` syntax in calling tests test.steps.append({ "force_update": { "current_slot": int(current_slot), - "checks": get_checks(spec, test.store), + "checks": get_checks(test.s_spec, test.store), } }) -def emit_update(test, spec, state, block, attested_state, attested_block, finalized_block, with_next=True): - update = spec.create_light_client_update(state, block, attested_state, attested_block, finalized_block) +def emit_update(test, spec, state, block, attested_state, attested_block, finalized_block, with_next=True, phases=None): + data_fork_version = spec.compute_fork_version(spec.compute_epoch_at_slot(attested_block.message.slot)) + data_fork_digest = spec.compute_fork_digest(data_fork_version, test.genesis_validators_root) + d_spec = get_spec_for_fork_version(spec, data_fork_version, phases) + data = d_spec.create_light_client_update(state, block, attested_state, attested_block, finalized_block) if not with_next: - update.next_sync_committee = spec.SyncCommittee() - update.next_sync_committee_branch = \ + data.next_sync_committee = spec.SyncCommittee() + data.next_sync_committee_branch = \ [spec.Bytes32() for _ in range(spec.floorlog2(spec.NEXT_SYNC_COMMITTEE_INDEX))] current_slot = state.slot - spec.process_light_client_update(test.store, update, current_slot, test.genesis_validators_root) - yield get_update_file_name(spec, update), update + upgraded = upgrade_lc_update_to_store(d_spec, test.s_spec, data) + test.s_spec.process_light_client_update(test.store, upgraded, current_slot, test.genesis_validators_root) + + yield get_update_file_name(d_spec, data), data test.steps.append({ "process_update": { - "update": get_update_file_name(spec, update), + "update_fork_digest": encode_hex(data_fork_digest), + "update": get_update_file_name(d_spec, data), "current_slot": int(current_slot), - "checks": get_checks(spec, test.store), + "checks": get_checks(test.s_spec, test.store), + } + }) + return upgraded + + +def emit_upgrade_store(test, new_s_spec, phases=None): + test.store = upgrade_lc_store_to_new_spec(test.s_spec, new_s_spec, test.store) + test.s_spec = new_s_spec + store_fork_version = get_store_fork_version(test.s_spec) + store_fork_digest = test.s_spec.compute_fork_digest(store_fork_version, test.genesis_validators_root) + + yield from [] # Consistently enable `yield from` syntax in calling tests + test.steps.append({ + "process_upgrade_store": { + "store_fork_digest": encode_hex(store_fork_digest), + "checks": get_checks(test.s_spec, test.store), } }) - return update def compute_start_slot_at_sync_committee_period(spec, sync_committee_period): @@ -141,10 +284,10 @@ def test_light_client_sync(spec, state): sync_aggregate, _ = get_sync_aggregate(spec, state) block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate) yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block) - assert spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot assert test.store.next_sync_committee == finalized_state.next_sync_committee assert test.store.best_valid_update is None - assert spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot # Advance to next sync committee period # ``` @@ -167,10 +310,10 @@ def test_light_client_sync(spec, state): sync_aggregate, _ = get_sync_aggregate(spec, state) block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate) yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block) - assert spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot assert test.store.next_sync_committee == finalized_state.next_sync_committee assert test.store.best_valid_update is None - assert spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot # Edge case: Signature in next period # ``` @@ -193,10 +336,10 @@ def test_light_client_sync(spec, state): sync_aggregate, _ = get_sync_aggregate(spec, state) block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate) yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block) - assert spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot assert test.store.next_sync_committee == finalized_state.next_sync_committee assert test.store.best_valid_update is None - assert spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot # Edge case: Finalized header not included # ``` @@ -214,10 +357,10 @@ def test_light_client_sync(spec, state): sync_aggregate, _ = get_sync_aggregate(spec, state) block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate) update = yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block=None) - assert spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot assert test.store.next_sync_committee == finalized_state.next_sync_committee assert test.store.best_valid_update == update - assert spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot # Non-finalized case: Attested `next_sync_committee` is not finalized # ``` @@ -236,10 +379,10 @@ def test_light_client_sync(spec, state): sync_aggregate, _ = get_sync_aggregate(spec, state) block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate) update = yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block) - assert spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot assert test.store.next_sync_committee == finalized_state.next_sync_committee assert test.store.best_valid_update == update - assert spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot # Force-update using timeout # ``` @@ -278,7 +421,7 @@ def test_light_client_sync(spec, state): assert spec.get_lc_beacon_slot(test.store.finalized_header) == store_state.slot assert test.store.next_sync_committee == store_state.next_sync_committee assert test.store.best_valid_update == update - assert spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot # Edge case: Finalized header older than store # ``` @@ -299,12 +442,12 @@ def test_light_client_sync(spec, state): assert spec.get_lc_beacon_slot(test.store.finalized_header) == store_state.slot assert test.store.next_sync_committee == store_state.next_sync_committee assert test.store.best_valid_update == update - assert spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot yield from emit_force_update(test, spec, state) assert spec.get_lc_beacon_slot(test.store.finalized_header) == attested_state.slot assert test.store.next_sync_committee == attested_state.next_sync_committee assert test.store.best_valid_update is None - assert spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot # Advance to next sync committee period # ``` @@ -327,10 +470,10 @@ def test_light_client_sync(spec, state): sync_aggregate, _ = get_sync_aggregate(spec, state) block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate) yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block) - assert spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot assert test.store.next_sync_committee == finalized_state.next_sync_committee assert test.store.best_valid_update is None - assert spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot # Finish test yield from finish_test(test) @@ -383,10 +526,10 @@ def test_advance_finality_without_sync_committee(spec, state): sync_aggregate, _ = get_sync_aggregate(spec, state) block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate) yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block) - assert spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot assert test.store.next_sync_committee == finalized_state.next_sync_committee assert test.store.best_valid_update is None - assert spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot # Advance finality into next sync committee period, but omit `next_sync_committee` transition_to(spec, state, compute_start_slot_at_next_sync_committee_period(spec, state)) @@ -402,10 +545,10 @@ def test_advance_finality_without_sync_committee(spec, state): sync_aggregate, _ = get_sync_aggregate(spec, state) block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate) yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block, with_next=False) - assert spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot assert not spec.is_next_sync_committee_known(test.store) assert test.store.best_valid_update is None - assert spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot # Advance finality once more, with `next_sync_committee` still unknown past_state = finalized_state @@ -422,21 +565,162 @@ def test_advance_finality_without_sync_committee(spec, state): assert spec.get_lc_beacon_slot(test.store.finalized_header) == past_state.slot assert not spec.is_next_sync_committee_known(test.store) assert test.store.best_valid_update == update - assert spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot # Apply `LightClientUpdate` with `finalized_header` but no `next_sync_committee` yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block, with_next=False) - assert spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot assert not spec.is_next_sync_committee_known(test.store) assert test.store.best_valid_update is None - assert spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot # Apply full `LightClientUpdate`, supplying `next_sync_committee` yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block) - assert spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot + assert test.store.next_sync_committee == finalized_state.next_sync_committee + assert test.store.best_valid_update is None + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + + # Finish test + yield from finish_test(test) + + +@with_phases(phases=[BELLATRIX], other_phases=[CAPELLA]) +@spec_test +@with_config_overrides({ + 'CAPELLA_FORK_EPOCH': 3, # `setup_test` advances to epoch 2 +}, emit=False) +@with_state +@with_matching_spec_config(emitted_fork=CAPELLA) +@with_presets([MINIMAL], reason="too slow") +def test_capella_fork(spec, phases, state): + # Start test + test = yield from setup_test(spec, state, phases=phases) + + # Initial `LightClientUpdate` + finalized_block = spec.SignedBeaconBlock() + finalized_block.message.state_root = state.hash_tree_root() + finalized_state = state.copy() + attested_block = state_transition_with_full_block(spec, state, True, True) + attested_state = state.copy() + sync_aggregate, _ = get_sync_aggregate(spec, state) + block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate) + yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block, phases=phases) + assert test.s_spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot + assert test.store.next_sync_committee == finalized_state.next_sync_committee + assert test.store.best_valid_update is None + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + + # Jump to two slots before Capella + transition_to(spec, state, spec.compute_start_slot_at_epoch(phases[CAPELLA].config.CAPELLA_FORK_EPOCH) - 4) + attested_block = state_transition_with_full_block(spec, state, True, True) + attested_state = state.copy() + sync_aggregate, _ = get_sync_aggregate(spec, state) + block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate) + update = \ + yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block, phases=phases) + assert test.s_spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot + assert test.store.next_sync_committee == finalized_state.next_sync_committee + assert test.store.best_valid_update == update + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + + # Perform `LightClientStore` upgrade + yield from emit_upgrade_store(test, phases[CAPELLA], phases=phases) + update = test.store.best_valid_update + + # Final slot before Capella, check that importing the Altair format still works + attested_block = block.copy() + attested_state = state.copy() + sync_aggregate, _ = get_sync_aggregate(spec, state) + block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate) + yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block, phases=phases) + assert test.s_spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot + assert test.store.next_sync_committee == finalized_state.next_sync_committee + assert test.store.best_valid_update == update + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + + # Upgrade to Capella, attested block is still before the fork + attested_block = block.copy() + attested_state = state.copy() + state, _ = do_fork(state, spec, phases[CAPELLA], phases[CAPELLA].config.CAPELLA_FORK_EPOCH, with_block=False) + spec = phases[CAPELLA] + sync_aggregate, _ = get_sync_aggregate(spec, state) + block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate) + yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block, phases=phases) + assert test.s_spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot + assert test.store.next_sync_committee == finalized_state.next_sync_committee + assert test.store.best_valid_update == update + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + assert test.s_spec.get_lc_execution_root(test.store.optimistic_header) == test.s_spec.Root() + + # Another block in Capella, this time attested block is after the fork + attested_block = block.copy() + attested_state = state.copy() + sync_aggregate, _ = get_sync_aggregate(spec, state) + block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate) + yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block, phases=phases) + assert test.s_spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot + assert test.store.next_sync_committee == finalized_state.next_sync_committee + assert test.store.best_valid_update == update + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + assert test.s_spec.get_lc_execution_root(test.store.optimistic_header) != test.s_spec.Root() + + # Jump to next epoch + transition_to(spec, state, spec.compute_start_slot_at_epoch(phases[CAPELLA].config.CAPELLA_FORK_EPOCH + 1) - 2) + attested_block = state_transition_with_full_block(spec, state, True, True) + attested_state = state.copy() + sync_aggregate, _ = get_sync_aggregate(spec, state) + block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate) + yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block, phases=phases) + assert test.s_spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot + assert test.s_spec.get_lc_execution_root(test.store.finalized_header) == test.s_spec.Root() + assert test.store.next_sync_committee == finalized_state.next_sync_committee + assert test.store.best_valid_update == update + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + assert test.s_spec.get_lc_execution_root(test.store.optimistic_header) != test.s_spec.Root() + + # Finalize it + finalized_block = block.copy() + finalized_state = state.copy() + _, _, state = next_slots_with_attestations(spec, state, 2 * spec.SLOTS_PER_EPOCH - 1, True, True) + attested_block = state_transition_with_full_block(spec, state, True, True) + attested_state = state.copy() + sync_aggregate, _ = get_sync_aggregate(spec, state) + block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate) + yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block, phases=phases) + assert test.s_spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot + assert test.s_spec.get_lc_execution_root(test.store.finalized_header) != test.s_spec.Root() + assert test.store.next_sync_committee == finalized_state.next_sync_committee + assert test.store.best_valid_update is None + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + assert test.s_spec.get_lc_execution_root(test.store.optimistic_header) != test.s_spec.Root() + + # Finish test + yield from finish_test(test) + + +@with_phases(phases=[ALTAIR, BELLATRIX], other_phases=[CAPELLA]) +@spec_test +@with_state +@with_matching_spec_config(emitted_fork=CAPELLA) +@with_presets([MINIMAL], reason="too slow") +def test_capella_store_with_legacy_data(spec, phases, state): + # Start test (Altair bootstrap but with a Capella store) + test = yield from setup_test(spec, state, s_spec=phases[CAPELLA], phases=phases) + + # Initial `LightClientUpdate` (check that it works with Capella store) + finalized_block = spec.SignedBeaconBlock() + finalized_block.message.state_root = state.hash_tree_root() + finalized_state = state.copy() + attested_block = state_transition_with_full_block(spec, state, True, True) + attested_state = state.copy() + sync_aggregate, _ = get_sync_aggregate(spec, state) + block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate) + yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block, phases=phases) + assert test.s_spec.get_lc_beacon_slot(test.store.finalized_header) == finalized_state.slot assert test.store.next_sync_committee == finalized_state.next_sync_committee assert test.store.best_valid_update is None - assert spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot + assert test.s_spec.get_lc_beacon_slot(test.store.optimistic_header) == attested_state.slot # Finish test yield from finish_test(test) diff --git a/tests/core/pyspec/eth2spec/test/capella/light_client/__init__.py b/tests/core/pyspec/eth2spec/test/capella/light_client/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..ca257a5a34 --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/capella/light_client/test_single_merkle_proof.py @@ -0,0 +1,31 @@ +from eth2spec.test.context import ( + spec_state_test, + with_capella_and_later, + with_test_suite_name, +) +from eth2spec.test.helpers.attestations import ( + state_transition_with_full_block, +) + + +@with_test_suite_name("BeaconBlockBody") +@with_capella_and_later +@spec_state_test +def test_execution_merkle_proof(spec, state): + block = state_transition_with_full_block(spec, state, True, False) + + yield "object", block.message.body + execution_branch = \ + spec.compute_merkle_proof_for_block_body(block.message.body, spec.EXECUTION_PAYLOAD_INDEX) + yield "proof", { + "leaf": "0x" + block.message.body.execution_payload.hash_tree_root().hex(), + "leaf_index": spec.EXECUTION_PAYLOAD_INDEX, + "branch": ['0x' + root.hex() for root in execution_branch] + } + assert spec.is_valid_merkle_branch( + leaf=block.message.body.execution_payload.hash_tree_root(), + branch=execution_branch, + depth=spec.floorlog2(spec.EXECUTION_PAYLOAD_INDEX), + index=spec.get_subtree_index(spec.EXECUTION_PAYLOAD_INDEX), + root=block.message.body.hash_tree_root(), + ) diff --git a/tests/formats/light_client/sync.md b/tests/formats/light_client/sync.md index 3beeec0ddc..5d6ac2b920 100644 --- a/tests/formats/light_client/sync.md +++ b/tests/formats/light_client/sync.md @@ -9,11 +9,15 @@ This series of tests provides reference test vectors for validating that a light ```yaml genesis_validators_root: Bytes32 -- string, hex encoded, with 0x prefix trusted_block_root: Bytes32 -- string, hex encoded, with 0x prefix +bootstrap_fork_digest: string -- Encoded `ForkDigest`-context of `bootstrap` +store_fork_digest: string -- Encoded `ForkDigest`-context of `store` object being tested ``` ### `bootstrap.ssz_snappy` -An SSZ-snappy encoded `bootstrap` object of type `LightClientBootstrap` to initialize a local `store` object of type `LightClientStore` using `initialize_light_client_store(trusted_block_rooot, bootstrap)`. +An SSZ-snappy encoded `bootstrap` object of type `LightClientBootstrap` to initialize a local `store` object of type `LightClientStore` with `store_fork_digest` using `initialize_light_client_store(trusted_block_rooot, bootstrap)`. The SSZ type can be determined from `bootstrap_fork_digest`. + +If `store_fork_digest` differs from `bootstrap_fork_digest`, the `bootstrap` object may need upgrading before initializing the store. ### `steps.yaml` @@ -27,10 +31,12 @@ Each step includes checks to verify the expected impact on the `store` object. finalized_header: { slot: int, -- Integer value from get_lc_beacon_slot(store.finalized_header) beacon_root: string, -- Encoded 32-byte value from get_lc_beacon_root(store.finalized_header) + execution_root: string, -- From Capella onward; get_lc_execution_root(store.finalized_header) } optimistic_header: { slot: int, -- Integer value from get_lc_beacon_slot(store.optimistic_header) beacon_root: string, -- Encoded 32-byte value from get_lc_beacon_root(store.optimistic_header) + execution_root: string, -- From Capella onward; get_lc_execution_root(store.optimistic_header) } ``` @@ -54,6 +60,7 @@ The function `process_light_client_update(store, update, current_slot, genesis_v ```yaml { + update_fork_digest: string -- Encoded `ForkDigest`-context of `update` update: string -- name of the `*.ssz_snappy` file to load as a `LightClientUpdate` object current_slot: int -- integer, decimal @@ -61,8 +68,24 @@ The function `process_light_client_update(store, update, current_slot, genesis_v } ``` +If `store_fork_digest` differs from `update_fork_digest`, the `update` object may need upgrading before initializing the store. + After this step, the `store` object may have been updated. +#### `process_upgrade_store` + +The `store` should be upgraded to reflect the new `store_fork_digest`: + +```yaml +{ + store_fork_digest: string -- Encoded `ForkDigest`-context of `store` + checks: {: value} -- the assertions. +} +``` + +After this step, the `store` object may have been updated. + + ## Condition A test-runner should initialize a local `LightClientStore` using the provided `bootstrap` object. It should then proceed to execute all the test steps in sequence. After each step, it should verify that the resulting `store` verifies against the provided `checks`. diff --git a/tests/generators/light_client/main.py b/tests/generators/light_client/main.py index 5d45bf39dd..54c09fae64 100644 --- a/tests/generators/light_client/main.py +++ b/tests/generators/light_client/main.py @@ -1,5 +1,5 @@ from eth2spec.test.helpers.constants import ALTAIR, BELLATRIX, CAPELLA, EIP4844 -from eth2spec.gen_helpers.gen_from_tests.gen import run_state_test_generators +from eth2spec.gen_helpers.gen_from_tests.gen import combine_mods, run_state_test_generators if __name__ == "__main__": @@ -9,7 +9,11 @@ 'update_ranking', ]} bellatrix_mods = altair_mods - capella_mods = bellatrix_mods + + _new_capella_mods = {key: 'eth2spec.test.capella.light_client.test_' + key for key in [ + 'single_merkle_proof', + ]} + capella_mods = combine_mods(_new_capella_mods, bellatrix_mods) eip4844_mods = capella_mods all_mods = {