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) |
- Core
- [Beacon chain changes](specs/capella/beacon-chain.md)
- [Capella fork](specs/capella/fork.md)
- Additions
- [Validator additions](specs/capella/validator.md)
- [P2P networking](specs/capella/p2p-interface.md)
|
-| EIP4844 (tentative) | - Core
- [Beacon Chain changes](specs/eip4844/beacon-chain.md)
- [EIP-4844 fork](specs/eip4844/fork.md)
- [Polynomial commitments](specs/eip4844/polynomial-commitments.md)
- [Fork choice changes](specs/eip4844/fork-choice.md)
- Additions
- [Honest validator guide changes](specs/eip4844/validator.md)
- [P2P networking](specs/eip4844/p2p-interface.md)
|
+| Capella (tentative) | - Core
- [Beacon chain changes](specs/capella/beacon-chain.md)
- [Capella fork](specs/capella/fork.md)
- Additions
- [Light client sync protocol changes](specs/capella/light-client/sync-protocol.md) ([fork](specs/capella/light-client/fork.md), [full node](specs/capella/light-client/full-node.md), [networking](specs/capella/light-client/p2p-interface.md))
- [Validator additions](specs/capella/validator.md)
- [P2P networking](specs/capella/p2p-interface.md)
|
+| EIP4844 (tentative) | - Core
- [Beacon Chain changes](specs/eip4844/beacon-chain.md)
- [EIP-4844 fork](specs/eip4844/fork.md)
- [Polynomial commitments](specs/eip4844/polynomial-commitments.md)
- [Fork choice changes](specs/eip4844/fork-choice.md)
- Additions
- [Light client sync protocol changes](specs/eip4844/light-client/sync-protocol.md) ([fork](specs/eip4844/light-client/fork.md), [full node](specs/eip4844/light-client/full-node.md), [networking](specs/eip4844/light-client/p2p-interface.md))
- [Honest validator guide changes](specs/eip4844/validator.md)
- [P2P networking](specs/eip4844/p2p-interface.md)
|
| Sharding (outdated) | - Core
- [Beacon Chain changes](specs/sharding/beacon-chain.md)
- Additions
- [P2P networking](specs/sharding/p2p-interface.md)
|
| Custody Game (outdated) | - Core
- [Beacon Chain changes](specs/custody_game/beacon-chain.md)
- Additions
- [Honest validator guide changes](specs/custody_game/validator.md)
| Dependent on sharding |
| Data Availability Sampling (outdated) | - Core
- [Core types and functions](specs/das/das-core.md)
- [Fork choice changes](specs/das/fork-choice.md)
- Additions
- [P2P Networking](specs/das/p2p-interface.md)
- [Sampling process](specs/das/sampling.md)
| - Dependent on sharding
- [Technical explainer](https://hackmd.io/@HWeNw8hNRimMm2m2GH56Cw/B1YJPGkpD)
|
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 = {