diff --git a/Makefile b/Makefile index ac769b200a..8604fac27a 100644 --- a/Makefile +++ b/Makefile @@ -26,11 +26,11 @@ 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) \ - $(wildcard $(SPEC_DIR)/eip4844/*.md) \ + $(wildcard $(SPEC_DIR)/eip4844/*.md) $(wildcard $(SPEC_DIR)/eip4844/**/*.md) \ $(wildcard $(SSZ_DIR)/*.md) COV_HTML_OUT=.htmlcov diff --git a/README.md b/README.md index bb69a452aa..ed8771cb00 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ Features are researched and developed in parallel, and then consolidated into se ### In-development Specifications | Code Name or Topic | Specs | Notes | | - | - | - | -| Capella (tentative) | | -| EIP4844 (tentative) | | +| Capella (tentative) | | +| EIP4844 (tentative) | | | Sharding (outdated) | | | Custody Game (outdated) | | Dependent on sharding | | Data Availability Sampling (outdated) | | | diff --git a/setup.py b/setup.py index 6fc26f3c0c..9102f819bb 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 # @@ -690,6 +705,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] @@ -982,6 +998,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 @@ -990,6 +1010,10 @@ def finalize_options(self): """ if self.spec_fork == EIP4844: self.md_doc_paths += """ + specs/eip4844/light-client/fork.md + specs/eip4844/light-client/full-node.md + specs/eip4844/light-client/p2p-interface.md + specs/eip4844/light-client/sync-protocol.md specs/eip4844/beacon-chain.md specs/eip4844/fork.md specs/eip4844/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..6dcc7578c2 --- /dev/null +++ b/specs/capella/light-client/fork.md @@ -0,0 +1,92 @@ +# 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: bellatrix.LightClientHeader) -> LightClientHeader: + return LightClientHeader( + beacon=pre.beacon, + ) +``` + +```python +def upgrade_lc_bootstrap_to_capella(pre: bellatrix.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: bellatrix.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: bellatrix.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: bellatrix.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: bellatrix.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..a406cd771b --- /dev/null +++ b/specs/capella/light-client/full-node.md @@ -0,0 +1,78 @@ +# 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: + epoch = compute_epoch_at_slot(block.message.slot) + + if epoch >= 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: + # Note that during fork transitions, `finalized_header` may still point to earlier forks. + # While Bellatrix blocks also contain an `ExecutionPayload` (minus `withdrawals_root`), + # it was not included in the corresponding light client data. To ensure compatibility + # with legacy data going through `upgrade_lc_header_to_capella`, leave out execution data. + 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..7c70ab11df --- /dev/null +++ b/specs/capella/light-client/sync-protocol.md @@ -0,0 +1,82 @@ +# 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) + - [Modified `LightClientHeader`](#modified-lightclientheader) +- [Helper functions](#helper-functions) + - [`get_lc_execution_root`](#get_lc_execution_root) + - [Modified `is_valid_light_client_header`](#modified-is_valid_light_client_header) + + + + +## 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 + +### Modified `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)] +``` + +## Helper functions + +### `get_lc_execution_root` + +```python +def get_lc_execution_root(header: LightClientHeader) -> Root: + epoch = compute_epoch_at_slot(header.beacon.slot) + + if epoch >= CAPELLA_FORK_EPOCH: + return hash_tree_root(header.execution) + + return Root() +``` + +### Modified `is_valid_light_client_header` + +```python +def is_valid_light_client_header(header: LightClientHeader) -> bool: + epoch = compute_epoch_at_slot(header.beacon.slot) + + if epoch < CAPELLA_FORK_EPOCH: + return ( + header.execution == ExecutionPayloadHeader() + and header.execution_branch == [Bytes32() for _ in range(floorlog2(EXECUTION_PAYLOAD_INDEX))] + ) + + 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, + ) +``` diff --git a/specs/eip4844/light-client/fork.md b/specs/eip4844/light-client/fork.md new file mode 100644 index 0000000000..2d5f74f467 --- /dev/null +++ b/specs/eip4844/light-client/fork.md @@ -0,0 +1,110 @@ +# EIP4844 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 [Capella specification](../../capella/light-client/sync-protocol.md) to EIP4844. This is necessary when processing pre-EIP4844 data with a post-EIP4844 `LightClientStore`. Note that the data being exchanged over the network protocols uses the original format. + +### Upgrading light client data + +A EIP4844 `LightClientStore` can still process earlier light client data. In order to do so, that pre-EIP4844 data needs to be locally upgraded to EIP4844 before processing. + +```python +def upgrade_lc_header_to_eip4844(pre: capella.LightClientHeader) -> LightClientHeader: + return LightClientHeader( + beacon=pre.beacon, + execution=ExecutionPayloadHeader( + parent_hash=pre.execution.parent_hash, + fee_recipient=pre.execution.fee_recipient, + state_root=pre.execution.state_root, + receipts_root=pre.execution.receipts_root, + logs_bloom=pre.execution.logs_bloom, + prev_randao=pre.execution.prev_randao, + block_number=pre.execution.block_number, + gas_limit=pre.execution.gas_limit, + gas_used=pre.execution.gas_used, + timestamp=pre.execution.timestamp, + extra_data=pre.execution.extra_data, + base_fee_per_gas=pre.execution.base_fee_per_gas, + block_hash=pre.execution.block_hash, + transactions_root=pre.execution.transactions_root, + withdrawals_root=pre.execution.withdrawals_root, + ), + execution_branch=pre.execution_branch, + ) +``` + +```python +def upgrade_lc_bootstrap_to_eip4844(pre: capella.LightClientBootstrap) -> LightClientBootstrap: + return LightClientBootstrap( + header=upgrade_lc_header_to_eip4844(pre.header), + current_sync_committee=pre.current_sync_committee, + current_sync_committee_branch=pre.current_sync_committee_branch, + ) +``` + +```python +def upgrade_lc_update_to_eip4844(pre: capella.LightClientUpdate) -> LightClientUpdate: + return LightClientUpdate( + attested_header=upgrade_lc_header_to_eip4844(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_eip4844(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_eip4844(pre: capella.LightClientFinalityUpdate) -> LightClientFinalityUpdate: + return LightClientFinalityUpdate( + attested_header=upgrade_lc_header_to_eip4844(pre.attested_header), + finalized_header=upgrade_lc_header_to_eip4844(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_eip4844(pre: capella.LightClientOptimisticUpdate) -> LightClientOptimisticUpdate: + return LightClientOptimisticUpdate( + attested_header=upgrade_lc_header_to_eip4844(pre.attested_header), + sync_aggregate=pre.sync_aggregate, + signature_slot=pre.signature_slot, + ) +``` + +### Upgrading the store + +Existing `LightClientStore` objects based on Capella MUST be upgraded to EIP4844 before EIP4844 based light client data can be processed. The `LightClientStore` upgrade MAY be performed before `EIP4844_FORK_EPOCH`. + +```python +def upgrade_lc_store_to_eip4844(pre: capella.LightClientStore) -> LightClientStore: + if pre.best_valid_update is None: + best_valid_update = None + else: + best_valid_update = upgrade_lc_update_to_eip4844(pre.best_valid_update) + return LightClientStore( + finalized_header=upgrade_lc_header_to_eip4844(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_eip4844(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/eip4844/light-client/full-node.md b/specs/eip4844/light-client/full-node.md new file mode 100644 index 0000000000..70983e1b39 --- /dev/null +++ b/specs/eip4844/light-client/full-node.md @@ -0,0 +1,74 @@ +# EIP4844 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) + - [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 EIP4844 upgrade. + +## Helper functions + +### Modified `block_to_light_client_header` + +```python +def block_to_light_client_header(block: SignedBeaconBlock) -> LightClientHeader: + epoch = compute_epoch_at_slot(block.message.slot) + + if epoch >= 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), + ) + + # [New in EIP4844] + if epoch >= EIP4844_FORK_EPOCH: + execution_header.excess_data_gas = payload.excess_data_gas + + execution_branch = compute_merkle_proof_for_block_body(block.message.body, EXECUTION_PAYLOAD_INDEX) + else: + # Note that during fork transitions, `finalized_header` may still point to earlier forks. + # While Bellatrix blocks also contain an `ExecutionPayload` (minus `withdrawals_root`), + # it was not included in the corresponding light client data. To ensure compatibility + # with legacy data going through `upgrade_lc_header_to_capella`, leave out execution data. + 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/eip4844/light-client/p2p-interface.md b/specs/eip4844/light-client/p2p-interface.md new file mode 100644 index 0000000000..f3d89c130d --- /dev/null +++ b/specs/eip4844/light-client/p2p-interface.md @@ -0,0 +1,105 @@ +# EIP4844 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 [Capella light client networking specification](../../capella/light-client/p2p-interface.md) is extended to exchange [EIP4844 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` | `capella.LightClientFinalityUpdate` | +| `EIP4844_FORK_VERSION` and later | `eip4844.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` | `capella.LightClientOptimisticUpdate` | +| `EIP4844_FORK_VERSION` and later | `eip4844.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` | `capella.LightClientBootstrap` | +| `EIP4844_FORK_VERSION` and later | `eip4844.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` | `capella.LightClientUpdate` | +| `EIP4844_FORK_VERSION` and later | `eip4844.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` | `capella.LightClientFinalityUpdate` | +| `EIP4844_FORK_VERSION` and later | `eip4844.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` | `capella.LightClientOptimisticUpdate` | +| `EIP4844_FORK_VERSION` and later | `eip4844.LightClientOptimisticUpdate` | diff --git a/specs/eip4844/light-client/sync-protocol.md b/specs/eip4844/light-client/sync-protocol.md new file mode 100644 index 0000000000..181ca14eb4 --- /dev/null +++ b/specs/eip4844/light-client/sync-protocol.md @@ -0,0 +1,87 @@ +# EIP4844 Light Client -- Sync Protocol + +**Notice**: This document is a work-in-progress for researchers and implementers. + +## Table of contents + + + + + +- [Introduction](#introduction) +- [Helper functions](#helper-functions) + - [Modified `get_lc_execution_root`](#modified-get_lc_execution_root) + - [Modified `is_valid_light_client_header`](#modified-is_valid_light_client_header) + + + + +## Introduction + +This upgrade updates light client data to include the EIP4844 changes to the [`ExecutionPayload`](../beacon-chain.md) structure. It extends the [Capella Light Client specifications](../../capella/light-client/sync-protocol.md). The [fork document](./fork.md) explains how to upgrade existing Capella based deployments to EIP4844. + +Additional documents describes the impact of the upgrade on certain roles: +- [Full node](./full-node.md) +- [Networking](./p2p-interface.md) + +## Helper functions + +### Modified `get_lc_execution_root` + +```python +def get_lc_execution_root(header: LightClientHeader) -> Root: + epoch = compute_epoch_at_slot(header.beacon.slot) + + # [New in EIP4844] + if epoch >= EIP4844_FORK_EPOCH: + return hash_tree_root(header.execution) + + # [Modified in EIP4844] + if epoch >= CAPELLA_FORK_EPOCH: + execution_header = capella.ExecutionPayloadHeader( + parent_hash=header.execution.parent_hash, + fee_recipient=header.execution.fee_recipient, + state_root=header.execution.state_root, + receipts_root=header.execution.receipts_root, + logs_bloom=header.execution.logs_bloom, + prev_randao=header.execution.prev_randao, + block_number=header.execution.block_number, + gas_limit=header.execution.gas_limit, + gas_used=header.execution.gas_used, + timestamp=header.execution.timestamp, + extra_data=header.execution.extra_data, + base_fee_per_gas=header.execution.base_fee_per_gas, + block_hash=header.execution.block_hash, + transactions_root=header.execution.transactions_root, + withdrawals_root=header.execution.withdrawals_root, + ) + return hash_tree_root(execution_header) + + return Root() +``` + +### Modified `is_valid_light_client_header` + +```python +def is_valid_light_client_header(header: LightClientHeader) -> bool: + epoch = compute_epoch_at_slot(header.beacon.slot) + + # [New in EIP4844] + if epoch < EIP4844_FORK_EPOCH: + if header.execution.excess_data_gas != uint256(0): + return False + + if epoch < CAPELLA_FORK_EPOCH: + return ( + header.execution == ExecutionPayloadHeader() + and header.execution_branch == [Bytes32() for _ in range(floorlog2(EXECUTION_PAYLOAD_INDEX))] + ) + + 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, + ) +``` 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 c7e6b5dbe1..664b4fb44f 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, EIP4844, + MINIMAL, + ALL_PHASES, +) +from eth2spec.test.helpers.fork_transition import ( + do_fork, +) +from eth2spec.test.helpers.forks import ( + is_post_capella, is_post_eip4844, + is_post_fork, +) from eth2spec.test.helpers.light_client import ( get_sync_aggregate, ) @@ -20,25 +36,150 @@ ) -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 needs_upgrade_to_eip4844(d_spec, s_spec): + return is_post_eip4844(s_spec) and not is_post_eip4844(d_spec) + + +def check_lc_header_equal(d_spec, s_spec, data, upgraded): + assert upgraded.beacon.slot == data.beacon.slot + assert upgraded.beacon.hash_tree_root() == data.beacon.hash_tree_root() + if is_post_capella(s_spec): + if is_post_capella(d_spec): + assert s_spec.get_lc_execution_root(upgraded) == d_spec.get_lc_execution_root(data) + else: + assert s_spec.get_lc_execution_root(upgraded) == s_spec.Root() + + +def check_lc_bootstrap_equal(d_spec, s_spec, data, upgraded): + check_lc_header_equal(d_spec, s_spec, data.header, upgraded.header) + assert upgraded.current_sync_committee == data.current_sync_committee + assert upgraded.current_sync_committee_branch == data.current_sync_committee_branch + + +def upgrade_lc_bootstrap_to_store(d_spec, s_spec, data): + upgraded = data + + if needs_upgrade_to_capella(d_spec, s_spec): + upgraded = s_spec.upgrade_lc_bootstrap_to_capella(upgraded) + check_lc_bootstrap_equal(d_spec, s_spec, data, upgraded) + + if needs_upgrade_to_eip4844(d_spec, s_spec): + upgraded = s_spec.upgrade_lc_bootstrap_to_eip4844(upgraded) + check_lc_bootstrap_equal(d_spec, s_spec, data, upgraded) + + return upgraded + + +def check_lc_update_equal(d_spec, s_spec, data, upgraded): + check_lc_header_equal(d_spec, s_spec, data.attested_header, upgraded.attested_header) + assert upgraded.next_sync_committee == data.next_sync_committee + assert upgraded.next_sync_committee_branch == data.next_sync_committee_branch + check_lc_header_equal(d_spec, s_spec, data.finalized_header, upgraded.finalized_header) + assert upgraded.sync_aggregate == data.sync_aggregate + assert upgraded.signature_slot == data.signature_slot + + +def upgrade_lc_update_to_store(d_spec, s_spec, data): + upgraded = data + + if needs_upgrade_to_capella(d_spec, s_spec): + upgraded = s_spec.upgrade_lc_update_to_capella(upgraded) + check_lc_update_equal(d_spec, s_spec, data, upgraded) + + if needs_upgrade_to_eip4844(d_spec, s_spec): + upgraded = s_spec.upgrade_lc_update_to_eip4844(upgraded) + check_lc_update_equal(d_spec, s_spec, data, upgraded) + return upgraded + + +def check_lc_store_equal(d_spec, s_spec, data, upgraded): + check_lc_header_equal(d_spec, s_spec, data.finalized_header, upgraded.finalized_header) + 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: + check_lc_update_equal(d_spec, s_spec, data.best_valid_update, upgraded.best_valid_update) + check_lc_header_equal(d_spec, s_spec, data.optimistic_header, upgraded.optimistic_header) + assert upgraded.previous_max_active_participants == data.previous_max_active_participants + assert upgraded.current_max_active_participants == data.current_max_active_participants + + +def upgrade_lc_store_to_new_spec(d_spec, s_spec, data): + upgraded = data + + if needs_upgrade_to_capella(d_spec, s_spec): + upgraded = s_spec.upgrade_lc_store_to_capella(upgraded) + check_lc_store_equal(d_spec, s_spec, data, upgraded) + + if needs_upgrade_to_eip4844(d_spec, s_spec): + upgraded = s_spec.upgrade_lc_store_to_eip4844(upgraded) + check_lc_store_equal(d_spec, s_spec, data, upgraded) + + 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_eip4844(s_spec): + return s_spec.config.EIP4844_FORK_VERSION + 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,19 +188,33 @@ 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(update.attested_header.beacon.hash_tree_root())}_{suffix1}{suffix2}" -def get_checks(store): +def get_checks(s_spec, store): + if is_post_capella(s_spec): + return { + "finalized_header": { + 'slot': int(store.finalized_header.beacon.slot), + 'beacon_root': encode_hex(store.finalized_header.beacon.hash_tree_root()), + 'execution_root': encode_hex(s_spec.get_lc_execution_root(store.finalized_header)), + }, + "optimistic_header": { + 'slot': int(store.optimistic_header.beacon.slot), + 'beacon_root': encode_hex(store.optimistic_header.beacon.hash_tree_root()), + 'execution_root': encode_hex(s_spec.get_lc_execution_root(store.optimistic_header)), + }, + } + return { "finalized_header": { 'slot': int(store.finalized_header.beacon.slot), @@ -74,35 +229,56 @@ def get_checks(store): 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(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(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({ + "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): @@ -440,3 +616,217 @@ def test_advance_finality_without_sync_committee(spec, state): # Finish test yield from finish_test(test) + + +def run_test_single_fork(spec, phases, state, fork): + # 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.store.finalized_header.beacon.slot == finalized_state.slot + assert test.store.next_sync_committee == finalized_state.next_sync_committee + assert test.store.best_valid_update is None + assert test.store.optimistic_header.beacon.slot == attested_state.slot + + # Jump to two slots before fork + fork_epoch = getattr(phases[fork].config, fork.upper() + '_FORK_EPOCH') + transition_to(spec, state, spec.compute_start_slot_at_epoch(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.store.finalized_header.beacon.slot == finalized_state.slot + assert test.store.next_sync_committee == finalized_state.next_sync_committee + assert test.store.best_valid_update == update + assert test.store.optimistic_header.beacon.slot == attested_state.slot + + # Perform `LightClientStore` upgrade + yield from emit_upgrade_store(test, phases[fork], phases=phases) + update = test.store.best_valid_update + + # Final slot before fork, check that importing the pre-fork 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.store.finalized_header.beacon.slot == finalized_state.slot + assert test.store.next_sync_committee == finalized_state.next_sync_committee + assert test.store.best_valid_update == update + assert test.store.optimistic_header.beacon.slot == attested_state.slot + + # Upgrade to post-fork spec, attested block is still before the fork + attested_block = block.copy() + attested_state = state.copy() + state, _ = do_fork(state, spec, phases[fork], fork_epoch, with_block=False) + spec = phases[fork] + 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.store.finalized_header.beacon.slot == finalized_state.slot + assert test.store.next_sync_committee == finalized_state.next_sync_committee + assert test.store.best_valid_update == update + assert test.store.optimistic_header.beacon.slot == attested_state.slot + + # Another block after the fork, 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.store.finalized_header.beacon.slot == finalized_state.slot + assert test.store.next_sync_committee == finalized_state.next_sync_committee + assert test.store.best_valid_update == update + assert test.store.optimistic_header.beacon.slot == attested_state.slot + + # Jump to next epoch + transition_to(spec, state, spec.compute_start_slot_at_epoch(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.store.finalized_header.beacon.slot == finalized_state.slot + assert test.store.next_sync_committee == finalized_state.next_sync_committee + assert test.store.best_valid_update == update + assert test.store.optimistic_header.beacon.slot == attested_state.slot + + # Finalize the fork + 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.store.finalized_header.beacon.slot == finalized_state.slot + assert test.store.next_sync_committee == finalized_state.next_sync_committee + assert test.store.best_valid_update is None + assert test.store.optimistic_header.beacon.slot == 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): + yield from run_test_single_fork(spec, phases, state, CAPELLA) + + +@with_phases(phases=[CAPELLA], other_phases=[EIP4844]) +@spec_test +@with_config_overrides({ + 'EIP4844_FORK_EPOCH': 3, # `setup_test` advances to epoch 2 +}, emit=False) +@with_state +@with_matching_spec_config(emitted_fork=EIP4844) +@with_presets([MINIMAL], reason="too slow") +def test_eip4844_fork(spec, phases, state): + yield from run_test_single_fork(spec, phases, state, EIP4844) + + +def run_test_multi_fork(spec, phases, state, fork_1, fork_2): + # Start test + test = yield from setup_test(spec, state, phases[fork_2], phases) + + # Set up so that finalized is from `spec`, ... + finalized_block = spec.SignedBeaconBlock() + finalized_block.message.state_root = state.hash_tree_root() + finalized_state = state.copy() + + # ..., attested is from `fork_1`, ... + fork_1_epoch = getattr(phases[fork_1].config, fork_1.upper() + '_FORK_EPOCH') + transition_to(spec, state, spec.compute_start_slot_at_epoch(fork_1_epoch) - 1) + state, _ = do_fork(state, spec, phases[fork_1], fork_1_epoch, with_block=False) + spec = phases[fork_1] + attested_block = state_transition_with_full_block(spec, state, True, True) + attested_state = state.copy() + + # ..., and signature is from `fork_2` + fork_2_epoch = getattr(phases[fork_2].config, fork_2.upper() + '_FORK_EPOCH') + transition_to(spec, state, spec.compute_start_slot_at_epoch(fork_2_epoch) - 1) + state, _ = do_fork(state, spec, phases[fork_2], fork_2_epoch, with_block=False) + spec = phases[fork_2] + sync_aggregate, _ = get_sync_aggregate(spec, state) + block = state_transition_with_full_block(spec, state, True, True, sync_aggregate=sync_aggregate) + + # Check that update applies + yield from emit_update(test, spec, state, block, attested_state, attested_block, finalized_block, phases=phases) + assert test.store.finalized_header.beacon.slot == finalized_state.slot + assert test.store.next_sync_committee == finalized_state.next_sync_committee + assert test.store.best_valid_update is None + assert test.store.optimistic_header.beacon.slot == attested_state.slot + + # Finish test + yield from finish_test(test) + + +@with_phases(phases=[BELLATRIX], other_phases=[CAPELLA, EIP4844]) +@spec_test +@with_config_overrides({ + 'CAPELLA_FORK_EPOCH': 3, # `setup_test` advances to epoch 2 + 'EIP4844_FORK_EPOCH': 4, +}, emit=False) +@with_state +@with_matching_spec_config(emitted_fork=EIP4844) +@with_presets([MINIMAL], reason="too slow") +def test_capella_eip4844_fork(spec, phases, state): + yield from run_test_multi_fork(spec, phases, state, CAPELLA, EIP4844) + + +def run_test_upgraded_store_with_legacy_data(spec, phases, state, fork): + # Start test (Legacy bootstrap with an upgraded store) + test = yield from setup_test(spec, state, phases[fork], phases) + + # Initial `LightClientUpdate` (check that the upgraded store can process it) + 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.store.finalized_header.beacon.slot == finalized_state.slot + assert test.store.next_sync_committee == finalized_state.next_sync_committee + assert test.store.best_valid_update is None + assert test.store.optimistic_header.beacon.slot == attested_state.slot + + # 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): + yield from run_test_upgraded_store_with_legacy_data(spec, phases, state, CAPELLA) + + +@with_phases(phases=[ALTAIR, BELLATRIX, CAPELLA], other_phases=[EIP4844]) +@spec_test +@with_state +@with_matching_spec_config(emitted_fork=EIP4844) +@with_presets([MINIMAL], reason="too slow") +def test_eip4844_store_with_legacy_data(spec, phases, state): + yield from run_test_upgraded_store_with_legacy_data(spec, phases, state, EIP4844) 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..8d3bf8e3cb --- /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 41aeec49b6..1706b4c162 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 to be upgraded 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 store.finalized_header.beacon.slot beacon_root: string, -- Encoded 32-byte value from store.finalized_header.beacon.hash_tree_root() + execution_root: string, -- From Capella onward; get_lc_execution_root(store.finalized_header) } optimistic_header: { slot: int, -- Integer value from store.optimistic_header.beacon.slot beacon_root: string, -- Encoded 32-byte value from store.optimistic_header.beacon.hash_tree_root() + 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,6 +68,21 @@ 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 to be upgraded before processing the update. + +After this step, the `store` object may have been updated. + +#### `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 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 = {