Skip to content

Electra Light Client -- Sync Protocol

Notice: This document is a work-in-progress for researchers and implementers.

Table of contents

Introduction

This upgrade updates light client data to include the Electra changes to the ExecutionPayload structure and to the generalized indices of surrounding containers. It extends the Deneb Light Client specifications. The fork document explains how to upgrade existing Deneb based deployments to Electra.

Additional documents describes the impact of the upgrade on certain roles: - Full node - Networking

Custom types

Name SSZ equivalent Description
FinalityBranch Vector[Bytes32, floorlog2(FINALIZED_ROOT_GINDEX_ELECTRA)] Merkle branch of finalized_checkpoint.root within BeaconState
CurrentSyncCommitteeBranch Vector[Bytes32, floorlog2(CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA)] Merkle branch of current_sync_committee within BeaconState
NextSyncCommitteeBranch Vector[Bytes32, floorlog2(NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA)] Merkle branch of next_sync_committee within BeaconState

Constants

Frozen constants

Existing GeneralizedIndex constants are frozen at their Altair values.

Name Value
FINALIZED_ROOT_GINDEX get_generalized_index(altair.BeaconState, 'finalized_checkpoint', 'root') (= 105)
CURRENT_SYNC_COMMITTEE_GINDEX get_generalized_index(altair.BeaconState, 'current_sync_committee') (= 54)
NEXT_SYNC_COMMITTEE_GINDEX get_generalized_index(altair.BeaconState, 'next_sync_committee') (= 55)

New constants

Name Value
FINALIZED_ROOT_GINDEX_ELECTRA get_generalized_index(BeaconState, 'finalized_checkpoint', 'root') (= 169)
CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA get_generalized_index(BeaconState, 'current_sync_committee') (= 86)
NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA get_generalized_index(BeaconState, 'next_sync_committee') (= 87)

Helper functions

Modified finalized_root_gindex_at_slot

1
2
3
4
5
6
7
def finalized_root_gindex_at_slot(slot: Slot) -> GeneralizedIndex:
    epoch = compute_epoch_at_slot(slot)

    # [Modified in Electra]
    if epoch >= ELECTRA_FORK_EPOCH:
        return FINALIZED_ROOT_GINDEX_ELECTRA
    return FINALIZED_ROOT_GINDEX

Modified current_sync_committee_gindex_at_slot

1
2
3
4
5
6
7
def current_sync_committee_gindex_at_slot(slot: Slot) -> GeneralizedIndex:
    epoch = compute_epoch_at_slot(slot)

    # [Modified in Electra]
    if epoch >= ELECTRA_FORK_EPOCH:
        return CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA
    return CURRENT_SYNC_COMMITTEE_GINDEX

Modified next_sync_committee_gindex_at_slot

1
2
3
4
5
6
7
def next_sync_committee_gindex_at_slot(slot: Slot) -> GeneralizedIndex:
    epoch = compute_epoch_at_slot(slot)

    # [Modified in Electra]
    if epoch >= ELECTRA_FORK_EPOCH:
        return NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA
    return NEXT_SYNC_COMMITTEE_GINDEX

Modified get_lc_execution_root

def get_lc_execution_root(header: LightClientHeader) -> Root:
    epoch = compute_epoch_at_slot(header.beacon.slot)

    # [New in Electra]
    if epoch >= ELECTRA_FORK_EPOCH:
        return hash_tree_root(header.execution)

    # [Modified in Electra]
    if epoch >= DENEB_FORK_EPOCH:
        execution_header = deneb.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,
            blob_gas_used=header.execution.blob_gas_used,
            excess_blob_gas=header.execution.excess_blob_gas,
        )
        return hash_tree_root(execution_header)

    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

def is_valid_light_client_header(header: LightClientHeader) -> bool:
    epoch = compute_epoch_at_slot(header.beacon.slot)

    if epoch < DENEB_FORK_EPOCH:
        if header.execution.blob_gas_used != uint64(0) or header.execution.excess_blob_gas != uint64(0):
            return False

    if epoch < CAPELLA_FORK_EPOCH:
        return (
            header.execution == ExecutionPayloadHeader()
            and header.execution_branch == ExecutionBranch()
        )

    return is_valid_merkle_branch(
        leaf=get_lc_execution_root(header),
        branch=header.execution_branch,
        depth=floorlog2(EXECUTION_PAYLOAD_GINDEX),
        index=get_subtree_index(EXECUTION_PAYLOAD_GINDEX),
        root=header.beacon.body_root,
    )