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:
- Extend. Each blob is erasure-coded to twice its length. Any half of the extended blob is enough to rebuild all of it.
- 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.
- 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
iof every row forms columni. - 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.
compute_matrix shows how the matrix relates to the blobs. Clients may store
cells and proofs however they like.
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 matrixNote: 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.
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: Rootdef 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.
| Check | What it rules out | Change in Glamsterdam |
|---|---|---|
| Structure | Wrong column index, no blobs, more blobs than the blob limit, cell/proof/commitment counts that don’t match | Commitments come from the bid; the blob-limit check moves to bid validation |
| Inclusion | Commitments that aren’t the block’s: the Merkle proof ties them to the signed header’s body_root | Removed: the commitments come from the block itself |
| KZG | Cells that don’t match their commitments; one batch check covers the whole column | Commitments 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 Truedef 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.
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:
DataColumnSidecarsByRootfetches particular columns of particular blocks. A node uses it when a sampled or custodied column didn’t arrive over gossip.DataColumnSidecarsByRangefetches 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.
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):
epoch | Chunk SSZ type |
|---|---|
FULU_FORK_EPOCH and later | fulu.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):
epoch | Chunk SSZ type |
|---|---|
FULU_FORK_EPOCH and later | fulu.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 fromsidecar.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_sidecaron thedata_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 matrixNote: 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.
| Name | Value | Description |
|---|---|---|
FIELD_ELEMENTS_PER_EXT_BLOB | 2 * FIELD_ELEMENTS_PER_BLOB | Number of field elements in a Reed-Solomon extended blob |
FIELD_ELEMENTS_PER_CELL | Uint64(64) | Number of field elements in a cell |
CELLS_PER_EXT_BLOB | FIELD_ELEMENTS_PER_EXT_BLOB // FIELD_ELEMENTS_PER_CELL | The number of cells in an extended blob |
[New in Deneb:EIP4844]
| Name | Value | Description |
|---|---|---|
MAX_REQUEST_BLOCKS_DENEB | Uint64(2**7) (= 128) | Maximum number of blocks in a single request |
MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS | Epoch(2**12) (= 4,096) | Minimum epoch range over which a node must serve blob sidecars |
BLOB_SIDECAR_SUBNET_COUNT | Uint64(6) | Number of blob sidecar subnets used in the gossipsub protocol |