Skip to content

Fulu -- Networking

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

Table of contents

Introduction

This document contains the consensus-layer networking specification for Fulu.

The specification of these changes continues in the same format as the network specifications of previous upgrades, and assumes them as pre-requisite.

Modifications in Fulu

Preset

Name Value Description
KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments'))) (= 4) Merkle proof index for blob_kzg_commitments

Configuration

[New in Fulu:EIP7594]

Name Value Description
DATA_COLUMN_SIDECAR_SUBNET_COUNT 128 The number of data column sidecar subnets used in the gossipsub protocol
MAX_REQUEST_DATA_COLUMN_SIDECARS MAX_REQUEST_BLOCKS_DENEB * NUMBER_OF_COLUMNS Maximum number of data column sidecars in a single request
MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS 2**12 (= 4096 epochs, ~18 days) The minimum epoch range over which a node must serve data column sidecars
MAX_REQUEST_BLOB_SIDECARS_FULU MAX_REQUEST_BLOCKS_DENEB * MAX_BLOBS_PER_BLOCK_FULU Maximum number of blob sidecars in a single request

Containers

DataColumnIdentifier

1
2
3
class DataColumnIdentifier(Container):
    block_root: Root
    index: ColumnIndex

Helpers

verify_data_column_sidecar
def verify_data_column_sidecar(sidecar: DataColumnSidecar) -> bool:
    """
    Verify if the data column sidecar is valid.
    """
    # The sidecar index must be within the valid range
    if sidecar.index >= NUMBER_OF_COLUMNS:
        return False

    # A sidecar for zero blobs is invalid
    if len(sidecar.kzg_commitments) == 0:
        return False

    # The column length must be equal to the number of commitments/proofs
    if len(sidecar.column) != len(sidecar.kzg_commitments) or len(sidecar.column) != len(sidecar.kzg_proofs):
        return False

    return True
verify_data_column_sidecar_kzg_proofs
def verify_data_column_sidecar_kzg_proofs(sidecar: DataColumnSidecar) -> bool:
    """
    Verify if the KZG proofs are correct.
    """
    # The column index also represents the cell index
    cell_indices = [CellIndex(sidecar.index)] * len(sidecar.column)

    # Batch verify that the cells match the corresponding commitments and proofs
    return verify_cell_kzg_proof_batch(
        commitments_bytes=sidecar.kzg_commitments,
        cell_indices=cell_indices,
        cells=sidecar.column,
        proofs_bytes=sidecar.kzg_proofs,
    )
verify_data_column_sidecar_inclusion_proof
def verify_data_column_sidecar_inclusion_proof(sidecar: DataColumnSidecar) -> bool:
    """
    Verify if the given KZG commitments included in the given beacon block.
    """
    gindex = get_subtree_index(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments'))
    return is_valid_merkle_branch(
        leaf=hash_tree_root(sidecar.kzg_commitments),
        branch=sidecar.kzg_commitments_inclusion_proof,
        depth=KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH,
        index=gindex,
        root=sidecar.signed_block_header.message.body_root,
    )
compute_subnet_for_data_column_sidecar
def compute_subnet_for_data_column_sidecar(column_index: ColumnIndex) -> SubnetID:
    return SubnetID(column_index % DATA_COLUMN_SIDECAR_SUBNET_COUNT)

MetaData

The MetaData stored locally by clients is updated with an additional field to communicate the custody subnet count.

1
2
3
4
5
6
(
  seq_number: uint64
  attnets: Bitvector[ATTESTATION_SUBNET_COUNT]
  syncnets: Bitvector[SYNC_COMMITTEE_SUBNET_COUNT]
  custody_subnet_count: uint64 # csc
)

Where

  • seq_number, attnets, and syncnets have the same meaning defined in the Altair document.
  • custody_subnet_count represents the node's custody subnet count. Clients MAY reject peers with a value less than CUSTODY_REQUIREMENT.

The gossip domain: gossipsub

Some gossip meshes are upgraded in the Fulu fork to support upgraded types.

Topics and messages

Global topics
beacon_block

Updated validation

  • [REJECT] The length of KZG commitments is less than or equal to the limitation defined in Consensus Layer -- i.e. validate that len(body.signed_beacon_block.message.blob_kzg_commitments) <= MAX_BLOBS_PER_BLOCK_FULU
Blob subnets
Deprecated blob_sidecar_{subnet_id}

blob_sidecar_{subnet_id} is deprecated.

data_column_sidecar_{subnet_id}

This topic is used to propagate column sidecars, where each column maps to some subnet_id.

The type of the payload of this topic is DataColumnSidecar.

The following validations MUST pass before forwarding the sidecar: DataColumnSidecar on the network, assuming the alias block_header = sidecar.signed_block_header.message:

  • [REJECT] The sidecar is valid as verified by verify_data_column_sidecar(sidecar).
  • [REJECT] The sidecar is for the correct subnet -- i.e. compute_subnet_for_data_column_sidecar(sidecar.index) == subnet_id.
  • [IGNORE] The sidecar is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) -- i.e. validate that block_header.slot <= current_slot (a client MAY queue future sidecars for processing at the appropriate slot).
  • [IGNORE] The sidecar is from a slot greater than the latest finalized slot -- i.e. validate that block_header.slot > compute_start_slot_at_epoch(state.finalized_checkpoint.epoch)
  • [REJECT] The proposer signature of sidecar.signed_block_header, is valid with respect to the block_header.proposer_index pubkey.
  • [IGNORE] The sidecar's block's parent (defined by block_header.parent_root) has been seen (via both gossip and non-gossip sources) (a client MAY queue sidecars for processing once the parent block is retrieved).
  • [REJECT] The sidecar's block's parent (defined by block_header.parent_root) passes validation.
  • [REJECT] The sidecar is from a higher slot than the sidecar's block's parent (defined by block_header.parent_root).
  • [REJECT] The current finalized_checkpoint is an ancestor of the sidecar's block -- i.e. get_checkpoint_block(store, block_header.parent_root, store.finalized_checkpoint.epoch) == store.finalized_checkpoint.root.
  • [REJECT] The sidecar's kzg_commitments field inclusion proof is valid as verified by verify_data_column_sidecar_inclusion_proof(sidecar).
  • [REJECT] The sidecar's column data is valid as verified by verify_data_column_sidecar_kzg_proofs(sidecar).
  • [IGNORE] The sidecar is the first sidecar for the tuple (block_header.slot, block_header.proposer_index, sidecar.index) with valid header signature, sidecar inclusion proof, and kzg proof.
  • [REJECT] The sidecar is proposed by the expected proposer_index for the block's slot in the context of the current shuffling (defined by block_header.parent_root/block_header.slot). If the proposer_index cannot immediately be verified against the expected shuffling, the sidecar MAY be queued for later processing while proposers for the block's branch are calculated -- in such a case do not REJECT, instead IGNORE this message.

Note: In the verify_data_column_sidecar_inclusion_proof(sidecar) check, for all the sidecars of the same block, it verifies against the same set of kzg_commitments of the given beacon block. Client can choose to cache the result of the arguments tuple (sidecar.kzg_commitments, sidecar.kzg_commitments_inclusion_proof, sidecar.signed_block_header).

The Req/Resp domain

Messages

BlobSidecarsByRoot v3

Protocol ID: /eth2/beacon_chain/req/blob_sidecars_by_root/3/

[Modified in Fulu:EIP7594]

The <context-bytes> field is calculated as context = compute_fork_digest(fork_version, genesis_validators_root):

fork_version Chunk SSZ type
FULU_FORK_VERSION fulu.BlobSidecar

Request Content:

1
2
3
(
  List[BlobIdentifier, MAX_REQUEST_BLOB_SIDECARS_FULU]
)

Response Content:

1
2
3
(
  List[BlobSidecar, MAX_REQUEST_BLOB_SIDECARS_FULU]
)

Updated validation

No more than MAX_REQUEST_BLOB_SIDECARS_FULU may be requested at a time.

BlobSidecarsByRange v3

Protocol ID: /eth2/beacon_chain/req/blob_sidecars_by_range/3/

[Modified in Fulu:EIP7594]

The <context-bytes> field is calculated as context = compute_fork_digest(fork_version, genesis_validators_root):

fork_version Chunk SSZ type
FULU_FORK_VERSION fulu.BlobSidecar

Request Content:

1
2
3
4
(
  start_slot: Slot
  count: uint64
)

Response Content:

1
2
3
(
  List[BlobSidecar, MAX_REQUEST_BLOB_SIDECARS_FULU]
)

Updated validation

Clients MUST respond with at least the blob sidecars of the first blob-carrying block that exists in the range, if they have it, and no more than MAX_REQUEST_BLOB_SIDECARS_FULU sidecars.

DataColumnSidecarsByRoot v1

Protocol ID: /eth2/beacon_chain/req/data_column_sidecars_by_root/1/

[New in Fulu:EIP7594]

The <context-bytes> field is calculated as context = compute_fork_digest(fork_version, genesis_validators_root):

fork_version Chunk SSZ type
FULU_FORK_VERSION fulu.DataColumnSidecar

Request Content:

1
2
3
(
  List[DataColumnIdentifier, MAX_REQUEST_DATA_COLUMN_SIDECARS]
)

Response Content:

1
2
3
(
    List[DataColumnSidecar, MAX_REQUEST_DATA_COLUMN_SIDECARS]
)

Requests sidecars by block root and index. The response is a list of DataColumnIdentifier whose length is less than or equal to the number of requests. It may be less in the case that the responding peer is missing blocks or sidecars.

Before consuming the next response chunk, the response reader SHOULD verify the data column sidecar is well-formatted through verify_data_column_sidecar, has valid inclusion proof through verify_data_column_sidecar_inclusion_proof, and is correct w.r.t. the expected KZG commitments through verify_data_column_sidecar_kzg_proofs.

No more than MAX_REQUEST_DATA_COLUMN_SIDECARS may be requested at a time.

The response MUST consist of zero or more response_chunk. Each successful response_chunk MUST contain a single DataColumnSidecar payload.

Clients MUST support requesting sidecars since minimum_request_epoch, where minimum_request_epoch = max(finalized_epoch, current_epoch - MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS, FULU_FORK_EPOCH). If any root in the request content references a block earlier than minimum_request_epoch, peers MAY respond with error code 3: ResourceUnavailable or not include the data column sidecar in the response.

Clients MUST respond with at least one sidecar, if they have it. Clients MAY limit the number of blocks and sidecars in the response.

Clients SHOULD include a sidecar in the response as soon as it passes the gossip validation rules. Clients SHOULD NOT respond with sidecars related to blocks that fail gossip validation rules. Clients SHOULD NOT respond with sidecars related to blocks that fail the beacon chain state transition

DataColumnSidecarsByRange v1

Protocol ID: /eth2/beacon_chain/req/data_column_sidecars_by_range/1/

The <context-bytes> field is calculated as context = compute_fork_digest(fork_version, genesis_validators_root):

fork_version Chunk SSZ type
FULU_FORK_VERSION fulu.DataColumnSidecar

Request Content:

1
2
3
4
5
(
  start_slot: Slot
  count: uint64
  columns: List[ColumnIndex, NUMBER_OF_COLUMNS]
)

Response Content:

1
2
3
(
  List[DataColumnSidecar, MAX_REQUEST_DATA_COLUMN_SIDECARS]
)

Requests data column sidecars in the slot range [start_slot, start_slot + count) of the given columns, leading up to the current head block as selected by fork choice.

Before consuming the next response chunk, the response reader SHOULD verify the data column sidecar is well-formatted through verify_data_column_sidecar, has valid inclusion proof through verify_data_column_sidecar_inclusion_proof, and is correct w.r.t. the expected KZG commitments through verify_data_column_sidecar_kzg_proofs.

DataColumnSidecarsByRange is primarily used to sync data columns that may have been missed on gossip and to sync within the MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS window.

The request MUST be encoded as an SSZ-container.

The response MUST consist of zero or more response_chunk. Each successful response_chunk MUST contain a single DataColumnSidecar payload.

Let data_column_serve_range be [max(current_epoch - MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS, FULU_FORK_EPOCH), current_epoch]. Clients MUST keep a record of data column sidecars seen on the epoch range data_column_serve_range where current_epoch is defined by the current wall-clock time, and clients MUST support serving requests of data columns on this range.

Peers that are unable to reply to data column sidecar requests within the range data_column_serve_range SHOULD respond with error code 3: ResourceUnavailable. Such peers that are unable to successfully reply to this range of requests MAY get descored or disconnected at any time.

Note: The above requirement implies that nodes that start from a recent weak subjectivity checkpoint MUST backfill the local data columns database to at least the range data_column_serve_range to be fully compliant with DataColumnSidecarsByRange requests.

Note: Although clients that bootstrap from a weak subjectivity checkpoint can begin participating in the networking immediately, other peers MAY disconnect and/or temporarily ban such an un-synced or semi-synced client.

Clients MUST respond with at least the data column sidecars of the first blob-carrying block that exists in the range, if they have it, and no more than MAX_REQUEST_DATA_COLUMN_SIDECARS sidecars.

Clients MUST include all data column sidecars of each block from which they include data column sidecars.

The following data column sidecars, where they exist, MUST be sent in (slot, column_index) order.

Slots that do not contain known data columns MUST be skipped, mimicking the behaviour of the BlocksByRange request. Only response chunks with known data columns should therefore be sent.

Clients MAY limit the number of data column sidecars in the response.

The response MUST contain no more than count * NUMBER_OF_COLUMNS data column sidecars.

Clients MUST respond with data columns sidecars from their view of the current fork choice -- that is, data column sidecars as included by blocks from the single chain defined by the current head. Of note, blocks from slots before the finalization MUST lead to the finalized block reported in the Status handshake.

Clients MUST respond with data column sidecars that are consistent from a single chain within the context of the request.

After the initial data column sidecar, clients MAY stop in the process of responding if their fork choice changes the view of the chain in the context of the request.

GetMetaData v3

Protocol ID: /eth2/beacon_chain/req/metadata/3/

No Request Content.

Response Content:

1
2
3
(
  MetaData
)

Requests the MetaData of a peer, using the new MetaData definition given above that is extended from Altair. Other conditions for the GetMetaData protocol are unchanged from the Altair p2p networking document.

The discovery domain: discv5

ENR structure

Custody group count

A new field is added to the ENR under the key cgc to facilitate custody data column discovery.

Key Value
cgc Custody group count, uint64 big endian integer with no leading zero bytes (0 is encoded as empty byte string)