Each request uses one libp2p stream and one exact protocol ID:
/eth2/beacon_chain/req/{method}/{version}/ssz_snappy
Multistream-select negotiates the full ID. The requester writes one SSZ-Snappy
request, closes its write side, and reads response chunks until the responder
closes, an error arrives, or the chunk fails validation. The requester MUST NOT
open more than MAX_CONCURRENT_REQUESTS = 2 streams for one protocol ID.
Chunk framing
Every response chunk starts with one unsigned byte:
| Code | Meaning | Payload |
|---|---|---|
0 |
Success |
optional context, length-prefixed SSZ-Snappy object |
1 |
InvalidRequest |
ErrorMessage |
2 |
ServerError |
ErrorMessage |
3 |
ResourceUnavailable |
ErrorMessage |
Codes 4 through 127 are reserved. Codes 128 through 255 are
method-specific. ErrorMessage is List[Byte, 256]; bytes need not be valid
UTF-8. Only the final chunk may have a non-zero result. A list response sends
one list element per successful chunk, not one SSZ list container.
The ssz_snappy header is an unsigned protobuf varint containing the raw SSZ
length. It must use at most ten bytes, fit the lower type or
MAX_PAYLOAD_SIZE bound, and be followed by exactly that many decompressed
bytes. A reader MUST reject early EOF, trailing bytes, or compressed input over
32 + n + n / 6. A requester treats a bad response as server behavior and
may reduce the peer's local score. A responder returns InvalidRequest for a
bad request header or payload.
Common methods and versions
The common method set includes /status/1/, /goodbye/1/, /ping/1/, /metadata/1/,
/beacon_blocks_by_range/1/, and /beacon_blocks_by_root/1/. Status/1
contains fork_digest, finalized root and epoch, head root, and head slot.
Ping/1 carries the metadata sequence number. Goodbye/1 carries a reason.
The method set also includes
/beacon_blocks_by_range/2/, /beacon_blocks_by_root/2/, and
/metadata/2/. Successful block chunks include a fixed four-byte fork-digest
context. The context epoch comes from the block slot and selects the fork
specific SignedBeaconBlock SSZ type. A client may serve v1 and v2 during
negotiation; v1 returns InvalidRequest for a payload selected by v2.
The method set also includes /status/2/ and
/metadata/3/. Status/2 extends Status/1 with
earliest_available_slot, which covers the earliest slot from which the peer
can serve all required blocks and sidecars. Metadata/3 extends the metadata
container with custody_group_count.
Gloas payload-envelope methods
The method set includes these exact protocol IDs:
/eth2/beacon_chain/req/execution_payload_envelopes_by_range/1/
/eth2/beacon_chain/req/execution_payload_envelopes_by_root/1/
The range request is (start_slot, count) and the response is a list of at
most MAX_REQUEST_PAYLOADS = 128 SignedExecutionPayloadEnvelope values. The
root request is an SSZ list of at most 128 block roots and returns zero or more
envelopes for those roots. A successful chunk carries the Gloas fork-digest
context and gloas.SignedExecutionPayloadEnvelope. The root method serves the
recent block-request window and may return ResourceUnavailable for an older
root.
Hegota inclusion-list method
The method set includes:
/eth2/beacon_chain/req/inclusion_lists_by_indices/1/
The SSZ-container request is (slot, inclusion_list_committee_root, indices: BitVector[INCLUSION_LIST_COMMITTEE_SIZE]). The response contains at
most MAX_REQUEST_INCLUSION_LIST = 16 SignedInclusionList values. A
successful chunk carries the Heze fork-digest context. A node serves lists
from max(current_slot - 1, compute_start_slot_at_epoch(HEZE_FORK_EPOCH))
and may return ResourceUnavailable for an older slot.
Request bounds
The block methods permit at most MAX_REQUEST_BLOCKS = 1,024 block roots or
range items.
Block methods use MAX_REQUEST_BLOCKS_DENEB = 128.
Blob-sidecar requests are bounded by compute_max_request_blob_sidecars() =
MAX_REQUEST_BLOCKS_DENEB × MAX_BLOBS_PER_BLOCK (768). A range response carries
at most count × MAX_BLOBS_PER_BLOCK sidecars.
From Pectra the function returns MAX_REQUEST_BLOCKS_DENEB × MAX_BLOBS_PER_BLOCK_ELECTRA (1,152), and the request bound and the count ×
response cap track the new per-block bound.
Light-client updates are capped at MAX_REQUEST_LIGHT_CLIENT_UPDATES = 128.
Fulu data-column requests derive their bound as
MAX_REQUEST_BLOCKS_DENEB * NUMBER_OF_COLUMNS.
Block serving covers the range returned by
compute_min_epochs_for_block_requests() and may return ResourceUnavailable
for earlier data. Sidecar, light-client, payload-envelope, and inclusion-list
pages define their own retention windows and context types.
The pinned consensus specifications define no protocol-wide numeric
TTFB_TIMEOUT or RESP_TIMEOUT. Implementations may use local timers, reset a
stream when a response is no longer useful, and must not treat a local timer as
an interoperable wire rule.
Failure handling
The responder validates the request before processing it, rate-limits by withholding complete chunks rather than emitting a false error, and closes its write side after the final chunk. The requester keeps a valid prefix from a partial stream but never treats it as a complete response. Unknown IDs fail negotiation; invalid contexts, lengths, decompression, SSZ, or response codes fail the stream. Retry and peer pruning remain local policy.
Scope and status
Req/Resp framing and error codes are shared by all methods. Data availability, light-client, inclusion-list, and payload-envelope methods remain separate surfaces even though they share this framing.