Skip to main content
netspecsEthereum networking

4.3.1.Data availability networking

Stage
In progress
Version
Pulled
consensus-specsdas-core.mdconsensus-specsp2p-interface.mdconsensus-specsp2p-interface.mdconsensus-specsp2p-interface.md
Related
ePBS envelopesConsensus synchronizationBlob streamingCell-level deltasPeerDAS custodyBlob lifecyclePartial data columnsEngine blob accesseth/72 sparse blobpoolEthereum networkingReq/Resp protocol

Before Fusaka, every consensus node downloaded every blob in full. That capped blob capacity at what the slowest node could download each slot. Fusaka (PeerDAS, EIP-7594) removes that cap: nodes download only part of each block’s blob data, and it can still be shown that the whole of it was published.

It works in three steps:

  1. Extend. Each blob is erasure-coded to twice its length. Any half of the extended blob is enough to rebuild all of it.
  2. Slice. The extended blobs of a block are cut into 128 columns. Each column holds the same small piece, one cell, of every blob, plus a proof that each cell matches its blob’s KZG commitment.
  3. Distribute. Each column travels on its own gossip subnet. A node custodies (stores and serves) at least 4 of 128 custody groups. Each slot it samples (downloads) at least 8 groups, its custody groups included. If every column it sampled arrives, it treats the block’s data as available.

Any 64 of the 128 columns rebuild every blob. To stop a rebuild, a proposer must withhold at least 65 columns. Then almost every node’s sample includes a withheld column, so the withholding shows up network-wide instead of fooling a few nodes.

This page covers what travels on the wire and how a node checks, requests and rebuilds it. Which columns a node is responsible for is on Custody. When columns are fetched and data is considered available is on Lifecycle.

The extended matrix

Think of a block’s blob data as a matrix with one row per blob and 128 columns.

  • A blob holds 4096 field elements (128 KiB). Extended, it holds 8192.
  • The extended blob is cut into 128 cells of 64 field elements, 2 KiB each. Cell i of every row forms column i.
  • Each cell has its own KZG proof. So a single cell can be checked against its blob’s commitment without the rest of the blob.

Nodes never hold rows. The design samples whole columns, and 50% of the columns rebuild every row, so rows are not needed.

In this construction, we extend the blobs using a one-dimensional erasure coding extension. The matrix comprises maximum MAX_BLOBS_PER_BLOCK rows and fixed NUMBER_OF_COLUMNS columns, with each row containing a Blob and its corresponding extension. compute_matrix demonstrates the relationship between blobs and the matrix, a potential method of storing cells/proofs.

class Cell(ByteVector):
    """
    The unit of extended blob data that has its own ``KZGProof``.
    """

    LENGTH = BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_CELL
class CellIndex(Uint64):
    """
    The index of a cell within an extended blob.
    """
class Cells(Vector[Cell]):
    """
    The cells of a single extended blob.
    """

    LENGTH = CELLS_PER_EXT_BLOB
class Proofs(Vector[KZGProof]):
    """
    The KZG proofs for the cells of a single extended blob.
    """

    LENGTH = CELLS_PER_EXT_BLOB
class ColumnIndex(Uint64):
    """
    The index of a column in the matrix of extended blob data.
    """
class RowIndex(Uint64):
    """
    The index of a row in the matrix of extended blob data.
    """

compute_matrix shows how the matrix relates to the blobs. Clients may store cells and proofs however they like.

class MatrixEntry(Container):
    cell: Cell
    kzg_proof: KZGProof
    column_index: ColumnIndex
    row_index: RowIndex
def compute_matrix(blobs: Sequence[Blob]) -> Sequence[MatrixEntry]:
    """
    Return the full, flattened sequence of matrix entries.

    This helper demonstrates the relationship between blobs and the matrix of cells/proofs.
    The data structure for storing cells/proofs is implementation-dependent.
    """
    matrix = []
    for blob_index, blob in enumerate(blobs):
        cells, proofs = kzg.compute_cells_and_kzg_proofs(blob)
        for cell_index, (cell, proof) in enumerate(zip(cells, proofs, strict=True)):
            matrix.append(
                MatrixEntry(
                    cell=cell,
                    kzg_proof=proof,
                    column_index=ColumnIndex(cell_index),
                    row_index=RowIndex(blob_index),
                )
            )
    return matrix

Note: The function kzg.compute_cells_and_kzg_proofs is defined in cryptography-specs with the following signature:

def compute_cells_and_kzg_proofs(blob: Blob) -> tuple[Cells, Proofs]:
    """
    Extend ``blob`` and return all the cells and proofs of the extended blob.
    """

Columns and sidecars

A column travels in a DataColumnSidecar. The sidecar carries the column’s cells and their proofs, plus whatever the receiver needs to tie them to a block.

Fusaka. The sidecar carries its own proof of origin: the proposer-signed block header, the block’s full list of blob commitments, and a Merkle proof that those commitments are in the block body. So a node can check a sidecar fully before the block has arrived.

Glamsterdam. With ePBS, the block no longer contains the payload. The blob commitments move into the builder’s bid, inside the beacon block. The sidecar drops the header, the commitments and the inclusion proof, and names its block by beacon_block_root and slot instead. The receiver reads the commitments from that block’s bid, so it must have seen the block first. Gloas also changes DataColumn and KZGProofs to progressive lists (EIP-7688). Their encoding is then the same whatever the blob limit, and the largest possible sidecar is computed from the largest limit in the blob schedule.

# [Modified in Gloas:EIP7688]
class DataColumn(ProgressiveList[Cell]):
    """
    A column of the extended blob data matrix, with at most one cell per blob.
    """
# [Modified in Gloas:EIP7688]
class KZGProofs(ProgressiveList[KZGProof]):
    """
    The KZG cell proofs of the cells held by a data column.
    """

Note: The signed_block_header, kzg_commitments, and kzg_commitments_inclusion_proof fields have been removed from DataColumnSidecar in Gloas as header and inclusion proof verifications are no longer required in Gloas. The KZG commitments are now located at block.body.signed_execution_payload_bid.message.blob_kzg_commitments where block is the BeaconBlock associated with beacon_block_root.

class DataColumnSidecar(Container):
    index: ColumnIndex
    # [Modified in Gloas:EIP7688]
    column: DataColumn
    # [Modified in Gloas:EIP7732]
    # Removed `kzg_commitments`
    # [Modified in Gloas:EIP7688]
    kzg_proofs: KZGProofs
    # [Modified in Gloas:EIP7732]
    # Removed `signed_block_header`
    # [Modified in Gloas:EIP7732]
    # Removed `kzg_commitments_inclusion_proof`
    # [New in Gloas:EIP7732]
    slot: Slot
    # [New in Gloas:EIP7732]
    beacon_block_root: Root
def compute_max_data_column_sidecar_size() -> Uint64:
    """
    Return the maximum size of a serialized ``DataColumnSidecar`` computed
    using the largest ``max_blobs_per_block`` value from the blob schedule,
    regardless of whether or not that is the current ``max_blobs_per_block``.
    """
    max_blobs = MAX_BLOBS_PER_BLOCK_ELECTRA
    for entry in BLOB_SCHEDULE:
        max_blobs = max(max_blobs, entry["MAX_BLOBS_PER_BLOCK"])

    sidecar = DataColumnSidecar(
        index=ColumnIndex(),
        column=DataColumn(data=[Cell()] * max_blobs),
        kzg_proofs=KZGProofs(data=[KZGProof()] * max_blobs),
        slot=Slot(),
        beacon_block_root=Root(),
    )
    return Uint64(len(ssz_serialize(sidecar)))

Checking a sidecar

A node checks a sidecar before using or forwarding it, whether it came from gossip or from a request. There are three checks, cheapest first. Each is one function, pulled below.

CheckWhat it rules outChange in Glamsterdam
StructureWrong column index, no blobs, more blobs than the blob limit, cell/proof/commitment counts that don’t matchCommitments come from the bid; the blob-limit check moves to bid validation
InclusionCommitments that aren’t the block’s: the Merkle proof ties them to the signed header’s body_rootRemoved: the commitments come from the block itself
KZGCells that don’t match their commitments; one batch check covers the whole columnCommitments come from the bid

All 128 sidecars of a block carry the same commitments and the same inclusion proof. A client can check the inclusion proof once per block and cache the result.

def verify_data_column_sidecar(
    sidecar: DataColumnSidecar,
    # [New in Gloas:EIP7732]
    kzg_commitments: BlobKZGCommitments,
) -> 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

    # [Modified in Gloas:EIP7732]
    # A sidecar for zero blobs is invalid
    if len(sidecar.column) == 0:
        return False

    # [Modified in Gloas:EIP7732]
    # The column length must be equal to the number of commitments
    if len(sidecar.column) != len(kzg_commitments):
        return False

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

    return True
def verify_data_column_sidecar_kzg_proofs(
    sidecar: DataColumnSidecar,
    # [New in Gloas:EIP7732]
    kzg_commitments: BlobKZGCommitments,
) -> 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 kzg.verify_cell_kzg_proof_batch(
        # [Modified in Gloas:EIP7732]
        commitments_bytes=kzg_commitments,
        cell_indices=cell_indices,
        cells=sidecar.column,
        proofs_bytes=sidecar.kzg_proofs,
    )

Column gossip

Column i is published on the gossip topic data_column_sidecar_{i mod DATA_COLUMN_SIDECAR_SUBNET_COUNT}. On mainnet there are 128 subnets, one per column. A node joins the subnets for its custody and sampling columns, so the topics it subscribes to reveal which columns it holds.

def compute_subnet_for_data_column_sidecar(column_index: ColumnIndex) -> SubnetID:
    return SubnetID(column_index % DATA_COLUMN_SIDECAR_SUBNET_COUNT)

Before forwarding a sidecar, a node runs the gossip validation below. It ignores a sidecar it can’t judge yet (a duplicate, a future slot, a block not yet seen) and rejects one that is provably wrong. A rejected sidecar lowers the sender’s peer score. An ignored one doesn’t.

What counts as a duplicate changes between the forks:

  • Fusaka: the key is (slot, proposer, column). The node checks the proposer’s signature and the inclusion proof itself, and forwards before the block arrives.
  • Glamsterdam: the key is (block root, column), and the block must be known first. A sidecar that arrives before its block may be queued. If it later fails validation, the peers that forwarded it are downscored after the fact.

The KZG commitments needed to verify a sidecar are now carried by the bid at block.body.signed_execution_payload_bid.message.blob_kzg_commitments, where block is the BeaconBlock with root sidecar.beacon_block_root.

Note: If the sidecar fails deferred validation, its forwarding peers MUST be downscored retroactively. If validation succeeds, the client MUST re-broadcast the sidecar.

def validate_data_column_sidecar_gossip(
    seen: Seen,
    store: Store,
    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.
    """
    # [IGNORE] This is the first sidecar seen for this block root and column index
    sidecar_key = (sidecar.beacon_block_root, sidecar.index)
    if sidecar_key in seen.data_column_sidecar_tuples:
        raise GossipIgnore("already seen sidecar for this block root and index")

    # [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 is_future_slot(store, sidecar.slot, current_time_ms):
        raise GossipIgnore("sidecar is from a future slot")

    # [IGNORE] A block for the sidecar has been seen (via gossip or non-gossip sources)
    # (MAY be queued until block is retrieved)
    # (SHOULD queue at least one sidecar per peer per subnet)
    if sidecar.beacon_block_root not in store.blocks:
        raise GossipIgnore("block for sidecar's beacon block root has not been seen")

    # [REJECT] The block for the sidecar passes validation
    if sidecar.beacon_block_root not in store.block_states:
        raise GossipReject("block for sidecar's beacon block root failed validation")

    block = store.blocks[sidecar.beacon_block_root]

    # [REJECT] The sidecar's slot matches the slot of the block
    if sidecar.slot != block.slot:
        raise GossipReject("sidecar's slot does not match block's slot")

    bid = block.body.signed_execution_payload_bid.message

    # [REJECT] The sidecar passes structural validation
    if not verify_data_column_sidecar(sidecar, bid.blob_kzg_commitments):
        raise GossipReject("invalid sidecar")

    # [REJECT] The sidecar's column data passes KZG verification
    if not verify_data_column_sidecar_kzg_proofs(sidecar, bid.blob_kzg_commitments):
        raise GossipReject("invalid sidecar kzg proofs")

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

Requesting columns

Gossip is the fast path. Req/Resp fills the gaps:

  • DataColumnSidecarsByRoot fetches particular columns of particular blocks. A node uses it when a sampled or custodied column didn’t arrive over gossip.
  • DataColumnSidecarsByRange fetches columns for a range of slots. A node uses it to sync, and to backfill after starting from a checkpoint.

Every node must serve its custody columns for the last MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS epochs, 4096 on mainnet (about 18 days), and never from before Fulu. A node started from a checkpoint must backfill that window before it fully meets the rule. The receiver runs the same three checks as for gossip on each response chunk.

class DataColumnIndices(List[ColumnIndex]):
    """
    The indices of the data columns being requested.
    """

    LIMIT = NUMBER_OF_COLUMNS
class DataColumnsByRootIdentifier(Container):
    block_root: Root
    columns: DataColumnIndices
class DataColumnsByRootIdentifiers(List[DataColumnsByRootIdentifier]):
    """
    The identifiers of the data column sidecars requested in a
    ``DataColumnSidecarsByRoot`` request.
    """

    LIMIT = MAX_REQUEST_BLOCKS_DENEB
class DataColumnSidecars(List[DataColumnSidecar]):
    """
    Data column sidecars returned in a ``DataColumnSidecarsByRange`` or
    ``DataColumnSidecarsByRoot`` response.
    """

    LIMIT = compute_max_request_data_column_sidecars()
def compute_max_request_data_column_sidecars() -> Uint64:
    """
    Return the maximum number of data column sidecars in a single request.
    """
    return MAX_REQUEST_BLOCKS_DENEB * NUMBER_OF_COLUMNS

By range

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

Request Content:

(
  start_slot: Slot
  count: Uint64
  columns: DataColumnIndices
)

Response Content:

(
  DataColumnSidecars
)

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

epochChunk SSZ type
FULU_FORK_EPOCH and laterfulu.DataColumnSidecar

By root

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

[New in Fulu:EIP7594]

Request Content:

(
  DataColumnsByRootIdentifiers
)

Response Content:

(
  DataColumnSidecars
)

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

epochChunk SSZ type
FULU_FORK_EPOCH and laterfulu.DataColumnSidecar

Gap in Glamsterdam. Gloas does not restate either request. The Fulu text above still tells the reader to check the inclusion proof and to take the context epoch from signed_block_header, and Gloas sidecars have neither. Until upstream fixes this, the natural reading is: take the epoch from sidecar.slot, skip the inclusion check, and verify against the commitments in the block’s bid.

Blob requests after Fusaka

Blob sidecars stop at the Fulu fork, but the history before it still has to be served until it falls out of the retention window. BlobSidecarsByRange and BlobSidecarsByRoot stay for that. A node answers them only for blocks before FULU_FORK_EPOCH, and they retire once MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS epochs have passed since the fork. The blob_sidecar_{subnet_id} gossip topics are retired at the fork itself.

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.

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.

Getting columns without the network

There are two ways a node can end up with columns it was never sent.

From its own execution client. Most blobs reach the execution client’s mempool before the block that includes them. When a node sees a new block, or the first column for it, it asks its execution client for every blob of the block with engine_getBlobsV2. If it gets all of them, it computes every column locally. It then treats each column as if it had arrived over gossip: it publishes the columns on the subnets it’s subscribed to and records them in its duplicate cache. This lets a block count as available even when its proposer was slow to publish. The Engine API method itself is on Blob access.

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

By reconstruction. A node holding at least 64 columns can rebuild the rest with recover_matrix. It then republishes the columns it’s subscribed to, and may drop those outside its custody. Nodes can wait a random short time before rebuilding, so they don’t all spend the CPU at once. When to give up on gossip and start rebuilding is left to the client.

def recover_matrix(
    partial_matrix: Sequence[MatrixEntry], blob_count: Uint64
) -> Sequence[MatrixEntry]:
    """
    Recover the full, flattened sequence of matrix entries.

    This helper demonstrates how to apply ``kzg.recover_cells_and_kzg_proofs``.
    The data structure for storing cells/proofs is implementation-dependent.
    """
    matrix = []
    for blob_index in range(blob_count):
        cell_indices = [e.column_index for e in partial_matrix if e.row_index == blob_index]
        cells = [e.cell for e in partial_matrix if e.row_index == blob_index]
        recovered_cells, recovered_proofs = kzg.recover_cells_and_kzg_proofs(cell_indices, cells)
        for cell_index, (cell, proof) in enumerate(
            zip(recovered_cells, recovered_proofs, strict=True)
        ):
            matrix.append(
                MatrixEntry(
                    cell=cell,
                    kzg_proof=proof,
                    column_index=ColumnIndex(cell_index),
                    row_index=RowIndex(blob_index),
                )
            )
    return matrix

Note: The function kzg.recover_cells_and_kzg_proofs is defined in cryptography-specs with the following signature:

def recover_cells_and_kzg_proofs(
    cell_indices: Sequence[CellIndex], cells: Sequence[Cell]
) -> tuple[Cells, Proofs]:
    """
    Recover all the cells and proofs of an extended blob given at least half of
    its cells.
    """

If the node obtains 50%+ of all the columns, it SHOULD reconstruct the full data matrix via the recover_matrix helper. Nodes MAY delay this reconstruction allowing time for other columns to arrive over the network. If delaying reconstruction, nodes may use a random delay in order to desynchronize reconstruction among nodes, thus reducing overall CPU load.

Once the node obtains a column through reconstruction, the node MUST expose the new column as if it had received it over the network. If the node is subscribed to the subnet corresponding to the column, it MUST send the reconstructed DataColumnSidecar to its topic mesh neighbors. If instead the node is not subscribed to the corresponding subnet, it SHOULD still expose the availability of the DataColumnSidecar as part of the gossip emission process. After exposing the reconstructed DataColumnSidecar to the network, the node MAY delete the DataColumnSidecar if it is not part of the node’s custody requirement.

Note: A node always maintains a matrix view of the rows and columns they are following, able to cross-reference and cross-seed in either direction.

Note: There are timing considerations to analyze — at what point does a node consider samples missing and choose to reconstruct and cross-seed.

Note: There may be anti-DoS and quality-of-service considerations around how to send samples and consider samples — is each individual sample a message or are they sent in aggregate forms.

Parameters

Mainnet values:

  • 128 columns, 128 subnets.
  • A 4096-epoch serving window for both columns and blobs.
  • An inclusion proof depth of 4.

BLOB_SIDECAR_SUBNET_COUNT and MAX_REQUEST_BLOCKS_DENEB apply only to the retired blob topics and requests, and to block requests.

NameValueDescription
FIELD_ELEMENTS_PER_EXT_BLOB2 * FIELD_ELEMENTS_PER_BLOBNumber of field elements in a Reed-Solomon extended blob
FIELD_ELEMENTS_PER_CELLUint64(64)Number of field elements in a cell
CELLS_PER_EXT_BLOBFIELD_ELEMENTS_PER_EXT_BLOB // FIELD_ELEMENTS_PER_CELLThe number of cells in an extended blob
NameValueDescription
NUMBER_OF_COLUMNSUint64(CELLS_PER_EXT_BLOB) (= 128)Number of columns in the extended data matrix
NameValueDescription
KZG_COMMITMENTS_INCLUSION_PROOF_DEPTHUint64(floorlog2(get_generalized_index(BeaconBlockBody, "blob_kzg_commitments"))) (= 4) Merkle proof index for blob_kzg_commitments

[New in Fulu:EIP7594]

NameValueDescription
DATA_COLUMN_SIDECAR_SUBNET_COUNTUint64(2**7) (= 128)Number of data column sidecar subnets used in the gossipsub protocol
MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTSEpoch(2**12) (= 4,096)Minimum epoch range over which a node must serve data column sidecars

[New in Deneb:EIP4844]

NameValueDescription
MAX_REQUEST_BLOCKS_DENEBUint64(2**7) (= 128)Maximum number of blocks in a single request
MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTSEpoch(2**12) (= 4,096)Minimum epoch range over which a node must serve blob sidecars
BLOB_SIDECAR_SUBNET_COUNTUint64(6)Number of blob sidecar subnets used in the gossipsub protocol