RLPx message encoding
The initial Hello message is encoded as follows:
frame-data = msg-id || msg-data
frame-size = length of frame-data, encoded as a 24bit big-endian integerwhere msg-id is an RLP-encoded integer identifying the message and msg-data is an RLP
list containing the message data.
All messages following Hello are compressed using the Snappy algorithm.
frame-data = msg-id || snappyCompress(msg-data)
frame-size = length of frame-data encoded as a 24bit big-endian integerNote that the frame-size of compressed messages refers to the compressed size of
msg-data. Since compressed messages may inflate to a very large size after
decompression, implementations should check for the uncompressed size of the data before
decoding the message. This is possible because the snappy format contains a length
header. Messages carrying uncompressed data larger than 16 MiB should be rejected by
closing the connection.
Consensus gossip encoding
Topics are post-fixed with an encoding. Encodings define how the payload of a gossipsub message is encoded.
ssz_snappy- All objects are SSZ-encoded and then compressed with Snappy block compression. Example: The beacon aggregate attestation topic string is/eth2/446a7232/beacon_aggregate_and_proof/ssz_snappy, the fork digest is446a7232and the data field of a gossipsub message is anAggregateAndProofthat has been SSZ-encoded and then compressed with Snappy.
Snappy has two formats: “block” and “frames” (streaming). Gossip messages remain relatively small (100s of bytes to 100s of kilobytes) so basic snappy block compression is used to avoid the additional overhead associated with snappy frames.
Implementations MUST use a single encoding for gossip. Changing an encoding will require coordination between participating implementations.
Gossip size limits
Size limits are placed both on the
RPCMsg
frame as well as the encoded payload in each
Message.
Clients MUST reject and MUST NOT emit or propagate messages whose size exceed the following limits:
- The size of the encoded
RPCMsg(including control messages, framing, topics, etc) must not exceedmax_message_size(). - The size of the compressed payload in the
Message.datafield must not exceedmax_compressed_len(MAX_PAYLOAD_SIZE). - The size of the uncompressed payload must not exceed
MAX_PAYLOAD_SIZEor the type-specific SSZ bound, whichever is lower.
Consensus Req/Resp encoding
The token of the negotiated protocol ID specifies the type of encoding to be used for the req/resp interaction. Only one value is possible at this time:
ssz_snappy: The contents are first SSZ-encoded and then compressed with Snappy frames compression. For objects containing a single field, only the field is SSZ-encoded not a container with a single field. For example, theBeaconBlocksByRootrequest is an SSZ-encoded list ofRoot’s. This encoding type MUST be supported by all clients.
SSZ-snappy framing and validation
The SimpleSerialize (SSZ) specification outlines how objects are SSZ-encoded.
To achieve snappy encoding on top of SSZ, we feed the serialized form of the object to the Snappy compressor on encoding. The inverse happens on decoding.
Snappy has two formats: “block” and “frames” (streaming). To support large requests and response chunks, snappy-framing is used.
Since snappy frame contents
have a maximum size of 65536 bytes
and frame headers are just identifier (1) + checksum (4) bytes, the expected
buffering of a single frame is acceptable.
Encoding-dependent header: Req/Resp protocols using the ssz_snappy
encoding strategy MUST encode the length of the raw SSZ bytes, encoded as an
unsigned
protobuf varint.
Writing: By first computing and writing the SSZ byte length, the SSZ encoder can then directly write the chunk contents to the stream. When Snappy is applied, it can be passed through a buffered Snappy writer to compress frame by frame.
Reading: After reading the expected SSZ byte length, the SSZ decoder can directly read the contents from the stream. When snappy is applied, it can be passed through a buffered Snappy reader to decompress frame by frame.
Before reading the payload, the header MUST be validated:
- The length-prefix MUST be encoded as an unsigned protobuf varint. It SHOULD be
minimally encoded (i.e., without any redundant bytes) and MUST not exceed 10
bytes in length, which is sufficient to represent any
Uint64value. The length-prefix MUST be decoded into a type which supports the full range ofUint64values. - The length-prefix is within the expected
size bounds derived from the payload SSZ type
or
MAX_PAYLOAD_SIZE, whichever is smaller.
After reading a valid header, the payload MAY be read, while maintaining the size constraints from the header.
A reader MUST NOT read more than max_compressed_len(n) bytes after reading the
SSZ length-prefix n from the header.
A reader MUST consider the following cases as invalid input:
- Any remaining bytes, after having read the
nSSZ bytes. An EOF is expected if more bytes are read than required. - An early EOF, before fully reading the declared length-prefix worth of SSZ bytes.
In case of an invalid input (header or payload), a reader MUST:
- From requests: send back an error message, response code
InvalidRequest. The request itself is ignored. - From responses: ignore the response, the response MUST be considered bad server behavior.
All messages that contain only a single field MUST be encoded directly as the type of that field and MUST NOT be encoded as an SSZ container.
Responses that are SSZ-lists (for example List[SignedBeaconBlock, ...]) send
their constituents individually as response_chunks. For example, the
List[SignedBeaconBlock, ...] response type sends zero or more
response_chunks. Each successful response_chunk contains a single
SignedBeaconBlock payload.