Skip to content

Fulu -- Networking

Introduction

This document contains the consensus-layer networking specifications 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
MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS 2**12 (= 4,096 epochs) The minimum epoch range over which a node must serve data column sidecars

Containers

New DataColumnsByRootIdentifier

1
2
3
class DataColumnsByRootIdentifier(Container):
    block_root: Root
    columns: List[ColumnIndex, NUMBER_OF_COLUMNS]

Helpers

Modified Seen

@dataclass
class Seen:
    proposer_slots: Set[Tuple[ValidatorIndex, Slot]]
    aggregator_epochs: Set[Tuple[ValidatorIndex, Epoch]]
    aggregate_data_roots: Dict[Tuple[Root, CommitteeIndex], Set[Tuple[boolean, ...]]]
    voluntary_exit_indices: Set[ValidatorIndex]
    proposer_slashing_indices: Set[ValidatorIndex]
    attester_slashing_indices: Set[ValidatorIndex]
    attestation_validator_epochs: Set[Tuple[ValidatorIndex, Epoch]]
    sync_contribution_aggregator_slots: Set[Tuple[ValidatorIndex, Slot, uint64]]
    sync_contribution_data: Dict[Tuple[Slot, Root, uint64], Set[Tuple[boolean, ...]]]
    sync_message_validator_slots: Set[Tuple[Slot, ValidatorIndex, uint64]]
    bls_to_execution_change_indices: Set[ValidatorIndex]
    # [Modified in Fulu:EIP7594]
    # Removed `blob_sidecar_tuples`
    # [New in Fulu:EIP7594]
    data_column_sidecar_tuples: Set[Tuple[Slot, ValidatorIndex, ColumnIndex]]
    # [New in Fulu]
    partial_data_column_headers: Dict[Root, PartialDataColumnHeader]

Modified compute_fork_version

def compute_fork_version(epoch: Epoch) -> Version:
    """
    Return the fork version at the given ``epoch``.
    """
    if epoch >= FULU_FORK_EPOCH:
        return FULU_FORK_VERSION
    if epoch >= ELECTRA_FORK_EPOCH:
        return ELECTRA_FORK_VERSION
    if epoch >= DENEB_FORK_EPOCH:
        return DENEB_FORK_VERSION
    if epoch >= CAPELLA_FORK_EPOCH:
        return CAPELLA_FORK_VERSION
    if epoch >= BELLATRIX_FORK_EPOCH:
        return BELLATRIX_FORK_VERSION
    if epoch >= ALTAIR_FORK_EPOCH:
        return ALTAIR_FORK_VERSION
    return GENESIS_FORK_VERSION

New compute_max_request_data_column_sidecars

1
2
3
4
5
def compute_max_request_data_column_sidecars() -> uint64:
    """
    Return the maximum number of data column sidecars in a single request.
    """
    return uint64(MAX_REQUEST_BLOCKS_DENEB * NUMBER_OF_COLUMNS)

New 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

    # Check that the sidecar respects the blob limit
    epoch = compute_epoch_at_slot(sidecar.signed_block_header.message.slot)
    if len(sidecar.kzg_commitments) > get_blob_parameters(epoch).max_blobs_per_block:
        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

New 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,
    )

New 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.
    """
    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=get_subtree_index(get_generalized_index(BeaconBlockBody, "blob_kzg_commitments")),
        root=sidecar.signed_block_header.message.body_root,
    )

New 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 group count.

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

Where

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

The gossip domain: gossipsub

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

Topics and messages

Global topics
Modified beacon_block

Note: This function is modified per EIP-7892. The block's KZG commitment count is bounded by get_blob_parameters(get_current_epoch(state)).max_blobs_per_block.

def validate_beacon_block_gossip(
    seen: Seen,
    store: Store,
    state: BeaconState,
    signed_beacon_block: SignedBeaconBlock,
    current_time_ms: uint64,
    block_payload_statuses: Optional[Dict[Root, PayloadValidationStatus]] = None,
) -> None:
    """
    Validate a SignedBeaconBlock for gossip propagation.
    Raises GossipIgnore or GossipReject on validation failure.
    """
    if block_payload_statuses is None:
        block_payload_statuses = {}
    block = signed_beacon_block.message
    execution_payload = block.body.execution_payload

    # [IGNORE] The block is not from a future slot
    # (MAY be queued for processing at the appropriate slot)
    if not is_not_from_future_slot(state, block.slot, current_time_ms):
        raise GossipIgnore("block is from a future slot")

    # [IGNORE] The block is from a slot greater than the latest finalized slot
    # (MAY choose to validate and store such blocks for additional purposes
    # -- e.g. slashing detection, archive nodes, etc)
    finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)
    if block.slot <= finalized_slot:
        raise GossipIgnore("block is not from a slot greater than the latest finalized slot")

    # [IGNORE] The block is the first block with valid signature received for the proposer for the slot
    if (block.proposer_index, block.slot) in seen.proposer_slots:
        raise GossipIgnore("block is not the first valid block for this proposer and slot")

    # [REJECT] The proposer index is a valid validator index
    if block.proposer_index >= len(state.validators):
        raise GossipReject("proposer index out of range")

    # [REJECT] The proposer signature is valid
    proposer = state.validators[block.proposer_index]
    domain = get_domain(state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(block.slot))
    signing_root = compute_signing_root(block, domain)
    if not bls.Verify(proposer.pubkey, signing_root, signed_beacon_block.signature):
        raise GossipReject("invalid proposer signature")

    # [IGNORE] The block's parent has been seen (via gossip or non-gossip sources)
    # (MAY be queued until parent is retrieved)
    if block.parent_root not in store.blocks:
        raise GossipIgnore("block's parent has not been seen")

    # [REJECT] The block's execution payload timestamp is correct with respect to the slot
    if execution_payload.timestamp != compute_time_at_slot(state, block.slot):
        raise GossipReject("incorrect execution payload timestamp")

    parent_payload_status = PAYLOAD_STATUS_NOT_VALIDATED
    if block.parent_root in block_payload_statuses:
        parent_payload_status = block_payload_statuses[block.parent_root]

    if block.parent_root not in store.block_states:
        if parent_payload_status == PAYLOAD_STATUS_NOT_VALIDATED:
            # [REJECT] The block's parent passes validation
            raise GossipReject("block's parent is invalid and EL result is unknown")

        # [IGNORE] The block's parent passes validation
        raise GossipIgnore("block's parent is invalid and EL result is known")

    # [IGNORE] The block's parent's execution payload passes validation
    if parent_payload_status == PAYLOAD_STATUS_INVALIDATED:
        raise GossipIgnore("block's parent is valid and EL result is invalid")

    # [REJECT] The block is from a higher slot than its parent
    if block.slot <= store.blocks[block.parent_root].slot:
        raise GossipReject("block is not from a higher slot than its parent")

    # [REJECT] The current finalized checkpoint is an ancestor of the block
    checkpoint_block = get_checkpoint_block(
        store, block.parent_root, store.finalized_checkpoint.epoch
    )
    if checkpoint_block != store.finalized_checkpoint.root:
        raise GossipReject("finalized checkpoint is not an ancestor of block")

    # [Modified in Fulu:EIP7892]
    # [REJECT] The length of KZG commitments is less than or equal to the limit
    max_blobs = get_blob_parameters(get_current_epoch(state)).max_blobs_per_block
    if len(block.body.blob_kzg_commitments) > max_blobs:
        raise GossipReject("too many blob kzg commitments")

    # [REJECT] The block is proposed by the expected proposer for the slot
    # (if shuffling is not available, IGNORE instead and MAY be queued for later)
    parent_state = store.block_states[block.parent_root].copy()
    process_slots(parent_state, block.slot)
    expected_proposer = get_beacon_proposer_index(parent_state)
    if block.proposer_index != expected_proposer:
        raise GossipReject("block proposer_index does not match expected proposer")

    # Mark this block as seen
    seen.proposer_slots.add((block.proposer_index, block.slot))
Blob subnets
Deprecated blob_sidecar_{subnet_id}

blob_sidecar_{subnet_id} is deprecated.

New data_column_sidecar_{subnet_id}

The data_column_sidecar_{subnet_id} topics, where each column index maps to some subnet_id, are used to propagate new data column sidecars to nodes on the network. Sidecars are sent in their entirety.

def validate_data_column_sidecar_gossip(
    seen: Seen,
    store: Store,
    state: BeaconState,
    sidecar: DataColumnSidecar,
    current_time_ms: uint64,
    subnet_id: SubnetID,
) -> None:
    """
    Validate a DataColumnSidecar for gossip propagation on a subnet.
    Raises GossipIgnore or GossipReject on validation failure.
    """
    block_header = sidecar.signed_block_header.message

    # [IGNORE] The sidecar is the first sidecar for the tuple
    # (block_header.slot, block_header.proposer_index, sidecar.index)
    sidecar_tuple = (block_header.slot, block_header.proposer_index, sidecar.index)
    if sidecar_tuple in seen.data_column_sidecar_tuples:
        raise GossipIgnore("already seen sidecar from this proposer for this slot and index")

    # [REJECT] The sidecar is valid as verified by verify_data_column_sidecar
    if not verify_data_column_sidecar(sidecar):
        raise GossipReject("invalid sidecar")

    # [REJECT] The sidecar is for the correct subnet
    if compute_subnet_for_data_column_sidecar(sidecar.index) != subnet_id:
        raise GossipReject("sidecar is for wrong subnet")

    # [IGNORE] The sidecar is not from a future slot
    # (MAY be queued for processing at the appropriate slot)
    if not is_not_from_future_slot(state, block_header.slot, current_time_ms):
        raise GossipIgnore("sidecar is from a future slot")

    # [IGNORE] The sidecar is from a slot greater than the latest finalized slot
    finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)
    if block_header.slot <= finalized_slot:
        raise GossipIgnore("sidecar is not from a slot greater than the latest finalized slot")

    # [REJECT] The proposer index is a valid validator index
    if block_header.proposer_index >= len(state.validators):
        raise GossipReject("proposer index out of range")

    # [REJECT] The proposer signature of sidecar.signed_block_header is valid
    proposer = state.validators[block_header.proposer_index]
    domain = get_domain(state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(block_header.slot))
    signing_root = compute_signing_root(block_header, domain)
    if not bls.Verify(proposer.pubkey, signing_root, sidecar.signed_block_header.signature):
        raise GossipReject("invalid proposer signature on sidecar block header")

    # [IGNORE] The sidecar's block's parent has been seen
    # (MAY be queued for processing once the parent block is retrieved)
    if block_header.parent_root not in store.blocks:
        raise GossipIgnore("sidecar's parent has not been seen")

    # [REJECT] The sidecar's block's parent passes validation
    if block_header.parent_root not in store.block_states:
        raise GossipReject("sidecar's parent failed validation")

    # [REJECT] The sidecar is from a higher slot than the sidecar's block's parent
    if block_header.slot <= store.blocks[block_header.parent_root].slot:
        raise GossipReject("sidecar is not from a higher slot than its parent")

    # [REJECT] The current finalized_checkpoint is an ancestor of the sidecar's block
    checkpoint_block = get_checkpoint_block(
        store, block_header.parent_root, store.finalized_checkpoint.epoch
    )
    if checkpoint_block != store.finalized_checkpoint.root:
        raise GossipReject("finalized checkpoint is not an ancestor of sidecar's block")

    # [REJECT] The sidecar is valid as verified by verify_data_column_sidecar_inclusion_proof
    if not verify_data_column_sidecar_inclusion_proof(sidecar):
        raise GossipReject("invalid sidecar inclusion proof")

    # [REJECT] The sidecar is valid as verified by verify_data_column_sidecar_kzg_proofs
    if not verify_data_column_sidecar_kzg_proofs(sidecar):
        raise GossipReject("invalid sidecar kzg proofs")

    # [REJECT] The sidecar is proposed by the expected proposer_index
    # (if shuffling is not available, IGNORE instead and MAY be queued for later)
    parent_state = store.block_states[block_header.parent_root].copy()
    process_slots(parent_state, block_header.slot)
    expected_proposer = get_beacon_proposer_index(parent_state)
    if block_header.proposer_index != expected_proposer:
        raise GossipReject("sidecar proposer_index does not match expected proposer")

    # Mark this data column sidecar as seen
    seen.data_column_sidecar_tuples.add(sidecar_tuple)

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).

Distributed blob publishing using blobs retrieved from local execution-layer client

Honest nodes SHOULD query engine_getBlobsV2 as soon as they receive a valid beacon_block or data_column_sidecar from gossip. If ALL blobs matching kzg_commitments are retrieved, they should convert the response to data columns, and import the result.

Implementers are encouraged to leverage this method to increase the likelihood of incorporating and attesting to the last block when its proposer is not able to publish data columns on time.

When clients use the local execution layer to retrieve blobs, they SHOULD skip verification of those blobs. When subsequently importing the blobs as data columns, they MUST behave as if the data_column_sidecar had been received via gossip. In particular, clients MUST:

  • Publish the corresponding data_column_sidecar on the data_column_sidecar_{subnet_id} topic if and only if they are subscribed to it, either due to custody requirements or additional sampling.
  • Update gossip rule related data structures (i.e. update the anti-equivocation cache).

The Req/Resp domain

Messages

Status v2

Protocol ID: /eth2/beacon_chain/req/status/2/

Request, Response Content:

1
2
3
4
5
6
7
8
9
(
  fork_digest: ForkDigest
  finalized_root: Root
  finalized_epoch: Epoch
  head_root: Root
  head_slot: Slot
  # [New in Fulu:EIP7594]
  earliest_available_slot: Slot
)

As seen by the client at the time of sending the message:

  • earliest_available_slot: The slot of earliest available block (SignedBeaconBlock).

Note: According to the definition of earliest_available_slot:

  • If the node is able to serve all blocks throughout the entire sidecars retention period (as defined by both MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS and MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS), but is NOT able to serve all sidecars during this period, it should advertise the earliest slot from which it can serve all sidecars.
  • If the node is able to serve all sidecars throughout the entire sidecars retention period (as defined by both MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS and MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS), it should advertise the earliest slot from which it can serve all blocks.
BlobSidecarsByRange v1

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

Deprecated as of FULU_FORK_EPOCH + MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS.

During the deprecation transition period:

  • Clients MUST respond with a list of blob sidecars from the range [min(current_epoch - MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS, FULU_FORK_EPOCH), FULU_FORK_EPOCH) if the requested range includes any epochs in this interval.
  • Clients MAY respond with an empty list if the requested range lies entirely at or after FULU_FORK_EPOCH.
  • Clients SHOULD NOT penalize peers for requesting blob sidecars from FULU_FORK_EPOCH.
BlobSidecarsByRoot v1

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

Deprecated as of FULU_FORK_EPOCH + MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS.

During the deprecation transition period:

  • Clients MUST respond with blob sidecars corresponding to block roots from the range [min(current_epoch - MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS, FULU_FORK_EPOCH), FULU_FORK_EPOCH) if any of the requested roots correspond to blocks in this interval.
  • Clients MAY respond with an empty list if all requested roots correspond to blocks at or after FULU_FORK_EPOCH.
  • Clients SHOULD NOT penalize peers for requesting blob sidecars from FULU_FORK_EPOCH.
DataColumnSidecarsByRange v1

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

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, compute_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 compute_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.

For each successful response_chunk, the ForkDigest context epoch is determined by compute_epoch_at_slot(data_column_sidecar.signed_block_header.message.slot).

Per fork_version = compute_fork_version(epoch):

epoch Chunk SSZ type
FULU_FORK_EPOCH and later fulu.DataColumnSidecar
DataColumnSidecarsByRoot v1

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

[New in Fulu:EIP7594]

Request Content:

1
2
3
(
  List[DataColumnsByRootIdentifier, MAX_REQUEST_BLOCKS_DENEB]
)

Response Content:

1
2
3
(
  List[DataColumnSidecar, compute_max_request_data_column_sidecars()]
)

Requests data column sidecars by block root and column indices. The response is a list of DataColumnSidecar whose length is less than or equal to requested_columns_count, where requested_columns_count = sum(len(r.columns) for r in request). 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 compute_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(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

For each successful response_chunk, the ForkDigest context epoch is determined by compute_epoch_at_slot(data_column_sidecar.signed_block_header.message.slot).

Per fork_version = compute_fork_version(epoch):

epoch Chunk SSZ type
FULU_FORK_EPOCH and later fulu.DataColumnSidecar
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.

BeaconBlocksByHead v1

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

[New in Fulu]

Request Content:

1
2
3
4
(
  beacon_root: Root
  count: uint64
)

Response Content:

1
2
3
(
  List[SignedBeaconBlock, MAX_REQUEST_BLOCKS_DENEB]
)

Requests beacon blocks along the ancestry of beacon_root, inclusive of the block at beacon_root, in descending slot order. The walk stops as soon as the response contains min(count, MAX_REQUEST_BLOCKS_DENEB) blocks or the next ancestor falls outside the epoch range that clients are required to serve (see below).

BeaconBlocksByHead is primarily used to backfill a contiguous range of blocks relative to a known head.

No more than MAX_REQUEST_BLOCKS_DENEB may be requested at a time.

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 SignedBeaconBlock payload.

Clients MUST support requesting blocks on the epoch range [current_epoch - compute_min_epochs_for_block_requests(), current_epoch]. If beacon_root references a block earlier than this range, peers MAY respond with error code 3: ResourceUnavailable or with no blocks in the response.

Clients MUST respond with at least one block, if they have the block at beacon_root. Clients MAY limit the number of blocks in the response.

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

For each successful response_chunk, the ForkDigest context epoch is determined by compute_epoch_at_slot(signed_beacon_block.message.slot).

Per fork_version = compute_fork_version(epoch):

fork_version Chunk SSZ type
GENESIS_FORK_VERSION phase0.SignedBeaconBlock
ALTAIR_FORK_VERSION altair.SignedBeaconBlock
BELLATRIX_FORK_VERSION bellatrix.SignedBeaconBlock
CAPELLA_FORK_VERSION capella.SignedBeaconBlock
DENEB_FORK_VERSION deneb.SignedBeaconBlock
ELECTRA_FORK_VERSION electra.SignedBeaconBlock
FULU_FORK_VERSION fulu.SignedBeaconBlock
GLOAS_FORK_VERSION gloas.SignedBeaconBlock

The discovery domain: discv5

ENR structure

eth2 field

[Modified in Fulu:EIP7892]

Note: The structure of ENRForkID has not changed but the field value computations have changed. Unless explicitly mentioned here, all specifications from phase0/p2p-interface.md#eth2-field carry over.

ENRs MUST carry a generic eth2 key with an 16-byte value of the node's current fork digest, next fork version, and next fork epoch to ensure connections are made with peers on the intended Ethereum network.

Key Value
eth2 SSZ ENRForkID

Specifically, the value of the eth2 key MUST be the following SSZ encoded object (ENRForkID):

1
2
3
4
5
(
  fork_digest: ForkDigest
  next_fork_version: Version
  next_fork_epoch: Epoch
)

The fields of ENRForkID are defined as:

  • fork_digest is compute_fork_digest(genesis_validators_root, epoch) where:
  • genesis_validators_root is the static Root found in state.genesis_validators_root.
  • epoch is the node's current epoch defined by the wall-clock time (not necessarily the epoch to which the node is sync).
  • next_fork_version is the fork version corresponding to the next planned fork at a future epoch. The fork version will only change for regular forks, not BPO forks. Note that it is possible for the blob schedule to define a change at the same epoch as a regular fork; this situation would be considered a regular fork. If no future fork is planned, set next_fork_version = current_fork_version to signal this fact.
  • next_fork_epoch is the epoch at which the next fork (whether a regular fork or a BPO fork) is planned. If no future fork is planned, set next_fork_epoch = FAR_FUTURE_EPOCH to signal this fact.
Custody group count

A new field is added to the ENR under the key cgc to facilitate custody data column discovery. This new field MUST be added once FULU_FORK_EPOCH is assigned any value other than FAR_FUTURE_EPOCH.

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

A new entry is added to the ENR under the key nfd, short for next fork digest. This entry communicates the digest of the next scheduled fork, regardless of whether it is a regular or a Blob-Parameters-Only fork. This new entry MUST be added once FULU_FORK_EPOCH is assigned any value other than FAR_FUTURE_EPOCH. Adding this entry prior to the Fulu upgrade will not impact peering as nodes will ignore unknown ENR entries and nfd mismatches do not cause disconnects.

If no next fork is scheduled, the nfd entry contains the default value for the type (i.e., the SSZ representation of a zero-filled array).

Key Value
nfd SSZ Bytes4 ForkDigest

When discovering and interfacing with peers, nodes MUST evaluate nfd alongside their existing consideration of the ENRForkID::next_* fields under the eth2 key, to form a more accurate view of the peer's intended next fork for the purposes of sustained peering. If there is a mismatch, the node MUST NOT disconnect before the fork boundary, but it MAY disconnect at/after the fork boundary.

Nodes unprepared to follow the Fulu upgrade will be unaware of nfd entries. However, their existing comparison of eth2 entries (concretely next_fork_epoch) is sufficient to detect upcoming divergence.

Peer scoring

Due to the deterministic custody functions, a node knows exactly what a peer should be able to respond to. In the event that a peer does not respond to samples of their custodied rows/columns, a node may downscore or disconnect from a peer.

Supernodes

A supernode is a node which subscribes to all data column sidecar subnets, custodies all data column sidecars, and performs reconstruction and cross-seeding. Being a supernode requires considerably higher bandwidth, storage, and computation resources. In order to reconstruct missing data, there must be at least one supernode on the network. Due to validator custody requirements, a node which is connected to validator(s) with a combined balance greater than or equal to 4096 ETH must be a supernode. Moreover, any node with the necessary resources may altruistically be a supernode. Therefore, there are expected to be many (hundreds) of supernodes on mainnet and it is likely (though not necessary) for a node to be connected to several of these by chance.