--- eip: 8178 title: Binary SSZ Transport for the Engine API description: Specifies a binary SSZ transport as an alternative to JSON-RPC for Engine API communication author: Giulio Rebuffo (@Giulio2002) discussions-to: https://ethereum-magicians.org/t/eip-8178-ssz-rest-engine-api-transport/27891 status: Draft type: Standards Track category: Core created: 2026-03-01 --- ## Abstract This EIP specifies a binary SSZ transport for Engine API communication between consensus layer (CL) and execution layer (EL) clients. The binary transport replaces JSON-RPC with resource-oriented REST endpoints and raw SSZ encoding for fast, efficient CL-EL communication. SSZ container definitions are provided for all Engine API structures and methods across all forks for backwards compatibility. ## Motivation Fast communication between the consensus layer and execution layer is critical for block propagation and validation timing. The JSON-RPC transport introduces unnecessary overhead in this critical path: - Binary data (hashes, addresses, transactions, blobs) is hex-encoded, doubling wire size. - JSON parsing and generation adds CPU overhead on both sides. - The CL uses SSZ natively, forcing a round-trip conversion (SSZ → JSON, then JSON → internal types) at the Engine API boundary. Binary SSZ eliminates all of this. The CL sends raw SSZ bytes over HTTP; the EL deserializes directly. No hex encoding, no JSON parsing, no intermediate representations. Payload sizes are reduced by ~50% compared to JSON-RPC, and serialization is no longer a bottleneck in the critical path between CL and EL. ## Specification The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174. ### Transport The binary SSZ transport uses resource-oriented REST over HTTP. Endpoints are organized by resource type (payloads, forkchoice, blobs) with per-endpoint versioning, following the same conventions as the Beacon API. #### Base URL All endpoints are served under the `/engine` prefix on the existing Engine API port (default `8551`): ``` http://localhost:8551/engine ``` #### Content Types | Header | Value | Description | | - | - | - | | `Content-Type` (request) | `application/octet-stream` | SSZ-encoded request container | | `Content-Type` (response) | `application/octet-stream` | SSZ-encoded response (success) | | `Content-Type` (response) | `text/plain` | Human-readable error message | | `Accept` (request) | `application/octet-stream` | Client accepts SSZ-encoded responses | Request bodies are the SSZ serialization of the endpoint's request container. Response bodies are the SSZ serialization of the endpoint's response type. GET requests with no body **SHOULD** include the `Accept` header to indicate SSZ preference. #### Authentication The binary transport uses the same JWT authentication as the JSON-RPC endpoint. All requests **MUST** include a valid JWT bearer token in the `Authorization` header: ``` Authorization: Bearer ``` All existing authentication requirements from the Engine API specification apply. #### Versioning Endpoints use path-based versioning following Beacon API conventions. Each endpoint includes a version number in its path (e.g., `/engine/v5/payloads`). The version number corresponds to the JSON-RPC method version it replaces: | SSZ REST Endpoint | JSON-RPC Equivalent | | - | - | | `POST /engine/v5/payloads` | `engine_newPayloadV5` | | `GET /engine/v6/payloads/{payload_id}` | `engine_getPayloadV6` | | `POST /engine/v4/forkchoice` | `engine_forkchoiceUpdatedV4` | | `POST /engine/v3/blobs` | `engine_getBlobsV3` | When a new fork introduces a new method version, a new versioned endpoint is added. Older versioned endpoints **MAY** be deprecated but **SHOULD** remain available for backwards compatibility. ### HTTP Status Codes #### Success | Status | Meaning | Usage | | - | - | - | | `200` | OK | Request succeeded, response body contains SSZ-encoded result | | `204` | No Content | Null result (e.g., syncing), empty body | #### Client Errors | Status | Meaning | Usage | | - | - | - | | `400` | Bad Request | Malformed SSZ encoding | | `401` | Unauthorized | Missing or invalid JWT token | | `404` | Not Found | Unknown payload ID | | `409` | Conflict | Invalid forkchoice state | | `413` | Request Too Large | Request exceeds maximum element count | | `422` | Unprocessable Entity | Invalid payload attributes | #### Server Errors | Status | Meaning | Usage | | - | - | - | | `500` | Internal Server Error | Unexpected server error | Error responses use `Content-Type: text/plain` with a human-readable error message body. ### Constants | Name | Value | Source | | - | - | - | | `MAX_BYTES_PER_TRANSACTION` | `2**30` (1,073,741,824) | [EIP-4844](./eip-4844.md) | | `MAX_TRANSACTIONS_PER_PAYLOAD` | `2**20` (1,048,576) | Bellatrix | | `MAX_WITHDRAWALS_PER_PAYLOAD` | `2**4` (16) | Capella | | `BYTES_PER_LOGS_BLOOM` | `256` | Bellatrix | | `MAX_EXTRA_DATA_BYTES` | `2**5` (32) | Bellatrix | | `MAX_BLOB_COMMITMENTS_PER_BLOCK` | `2**12` (4,096) | Deneb | | `FIELD_ELEMENTS_PER_BLOB` | `4096` | [EIP-4844](./eip-4844.md) | | `BYTES_PER_FIELD_ELEMENT` | `32` | [EIP-4844](./eip-4844.md) | | `CELLS_PER_EXT_BLOB` | `128` | [EIP-7594](./eip-7594.md) | | `MAX_PAYLOAD_BODIES_REQUEST` | `2**5` (32) | Shanghai | | `MAX_BLOB_HASHES_REQUEST` | `128` | Osaka | | `MAX_EXECUTION_REQUESTS` | `2**8` (256) | [EIP-7685](./eip-7685.md) | | `MAX_ERROR_MESSAGE_LENGTH` | `1024` | This specification | | `MAX_CLIENT_CODE_LENGTH` | `2` | This specification | | `MAX_CLIENT_NAME_LENGTH` | `64` | This specification | | `MAX_CLIENT_VERSION_LENGTH` | `64` | This specification | | `MAX_CLIENT_VERSIONS` | `4` | This specification | | `BLOB_SIZE` | `FIELD_ELEMENTS_PER_BLOB * BYTES_PER_FIELD_ELEMENT` (131,072) | Derived | ### SSZ Type Mappings Each JSON-encoded base type used in the Engine API maps to a specific SSZ type: | JSON-RPC Type | SSZ Type | | - | - | | `address` (20 bytes) | `Bytes20` | | `hash32` (32 bytes) | `Bytes32` | | `bytes8` (8 bytes) | `Bytes8` | | `bytes32` (32 bytes) | `Bytes32` | | `bytes48` (48 bytes) | `Bytes48` | | `bytes256` (256 bytes) | `ByteVector[256]` | | `uint64` | `uint64` | | `uint256` | `uint256` | | `BOOLEAN` | `boolean` | | `bytes` (variable-length) | `ByteList[MAX_LENGTH]` (context-dependent) | | `bytesMax32` (0 to 32 bytes) | `ByteList[32]` | | `Array of T` | `List[T, MAX_LENGTH]` (context-dependent) | | `T or null` | `List[T, 1]` | Nullable types are represented as `List[T, 1]` in SSZ encoding. An empty list (0 elements) denotes absence (`null`). A list with one element denotes presence. ### Container Definitions #### WithdrawalV1 ```python class WithdrawalV1(Container): index: uint64 validator_index: uint64 address: Bytes20 amount: uint64 ``` #### ExecutionPayloadV1 ```python class ExecutionPayloadV1(Container): parent_hash: Bytes32 fee_recipient: Bytes20 state_root: Bytes32 receipts_root: Bytes32 logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] prev_randao: Bytes32 block_number: uint64 gas_limit: uint64 gas_used: uint64 timestamp: uint64 extra_data: ByteList[MAX_EXTRA_DATA_BYTES] base_fee_per_gas: uint256 block_hash: Bytes32 transactions: List[ByteList[MAX_BYTES_PER_TRANSACTION], MAX_TRANSACTIONS_PER_PAYLOAD] ``` #### ExecutionPayloadV2 Extends `ExecutionPayloadV1` with `withdrawals`. ```python class ExecutionPayloadV2(Container): parent_hash: Bytes32 fee_recipient: Bytes20 state_root: Bytes32 receipts_root: Bytes32 logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] prev_randao: Bytes32 block_number: uint64 gas_limit: uint64 gas_used: uint64 timestamp: uint64 extra_data: ByteList[MAX_EXTRA_DATA_BYTES] base_fee_per_gas: uint256 block_hash: Bytes32 transactions: List[ByteList[MAX_BYTES_PER_TRANSACTION], MAX_TRANSACTIONS_PER_PAYLOAD] withdrawals: List[WithdrawalV1, MAX_WITHDRAWALS_PER_PAYLOAD] ``` #### ExecutionPayloadV3 Extends `ExecutionPayloadV2` with `blob_gas_used` and `excess_blob_gas`. ```python class ExecutionPayloadV3(Container): parent_hash: Bytes32 fee_recipient: Bytes20 state_root: Bytes32 receipts_root: Bytes32 logs_bloom: ByteVector[BYTES_PER_LOGS_BLOOM] prev_randao: Bytes32 block_number: uint64 gas_limit: uint64 gas_used: uint64 timestamp: uint64 extra_data: ByteList[MAX_EXTRA_DATA_BYTES] base_fee_per_gas: uint256 block_hash: Bytes32 transactions: List[ByteList[MAX_BYTES_PER_TRANSACTION], MAX_TRANSACTIONS_PER_PAYLOAD] withdrawals: List[WithdrawalV1, MAX_WITHDRAWALS_PER_PAYLOAD] blob_gas_used: uint64 excess_blob_gas: uint64 ``` #### PayloadStatusV1 The `status` field is encoded as a `uint8` enum. ```python class PayloadStatusV1(Container): status: uint8 latest_valid_hash: Bytes32 validation_error: ByteList[MAX_ERROR_MESSAGE_LENGTH] ``` *Note:* `latest_valid_hash` is all zeros when absent (e.g. when `status` is `SYNCING` or `ACCEPTED`). `validation_error` is empty when absent. | `status` value | Meaning | | - | - | | `0` | VALID | | `1` | INVALID | | `2` | SYNCING | | `3` | ACCEPTED | | `4` | INVALID_BLOCK_HASH | #### ForkchoiceStateV1 ```python class ForkchoiceStateV1(Container): head_block_hash: Bytes32 safe_block_hash: Bytes32 finalized_block_hash: Bytes32 ``` #### PayloadAttributesV1 ```python class PayloadAttributesV1(Container): timestamp: uint64 prev_randao: Bytes32 suggested_fee_recipient: Bytes20 ``` #### PayloadAttributesV2 Extends `PayloadAttributesV1` with `withdrawals`. ```python class PayloadAttributesV2(Container): timestamp: uint64 prev_randao: Bytes32 suggested_fee_recipient: Bytes20 withdrawals: List[WithdrawalV1, MAX_WITHDRAWALS_PER_PAYLOAD] ``` #### PayloadAttributesV3 Extends `PayloadAttributesV2` with `parent_beacon_block_root`. ```python class PayloadAttributesV3(Container): timestamp: uint64 prev_randao: Bytes32 suggested_fee_recipient: Bytes20 withdrawals: List[WithdrawalV1, MAX_WITHDRAWALS_PER_PAYLOAD] parent_beacon_block_root: Bytes32 ``` #### ForkchoiceUpdatedResponseV1 Used by all versions of `engine_forkchoiceUpdated`. ```python class ForkchoiceUpdatedResponseV1(Container): payload_status: PayloadStatusV1 payload_id: Bytes8 ``` *Note:* `payload_id` is all zeros when no payload building was initiated. #### ExecutionPayloadBodyV1 ```python class ExecutionPayloadBodyV1(Container): transactions: List[ByteList[MAX_BYTES_PER_TRANSACTION], MAX_TRANSACTIONS_PER_PAYLOAD] withdrawals: List[WithdrawalV1, MAX_WITHDRAWALS_PER_PAYLOAD] ``` *Note:* `withdrawals` is empty for pre-Shanghai blocks. #### BlobsBundleV1 ```python class BlobsBundleV1(Container): commitments: List[Bytes48, MAX_BLOB_COMMITMENTS_PER_BLOCK] proofs: List[Bytes48, MAX_BLOB_COMMITMENTS_PER_BLOCK] blobs: List[ByteVector[BLOB_SIZE], MAX_BLOB_COMMITMENTS_PER_BLOCK] ``` #### BlobsBundleV2 Proofs are cell proofs with `CELLS_PER_EXT_BLOB` proofs per blob. ```python class BlobsBundleV2(Container): commitments: List[Bytes48, MAX_BLOB_COMMITMENTS_PER_BLOCK] proofs: List[Bytes48, MAX_BLOB_COMMITMENTS_PER_BLOCK * CELLS_PER_EXT_BLOB] blobs: List[ByteVector[BLOB_SIZE], MAX_BLOB_COMMITMENTS_PER_BLOCK] ``` #### BlobAndProofV1 ```python class BlobAndProofV1(Container): blob: ByteVector[BLOB_SIZE] proof: Bytes48 ``` #### BlobAndProofV2 ```python class BlobAndProofV2(Container): blob: ByteVector[BLOB_SIZE] proofs: List[Bytes48, CELLS_PER_EXT_BLOB] ``` #### TransitionConfigurationV1 Deprecated in Cancun. ```python class TransitionConfigurationV1(Container): terminal_total_difficulty: uint256 terminal_block_hash: Bytes32 terminal_block_number: uint64 ``` #### GetPayloadResponseV2 ```python class GetPayloadResponseV2(Container): execution_payload: ExecutionPayloadV2 block_value: uint256 ``` *Note:* Pre-Shanghai payloads have an empty `withdrawals` list. #### GetPayloadResponseV3 ```python class GetPayloadResponseV3(Container): execution_payload: ExecutionPayloadV3 block_value: uint256 blobs_bundle: BlobsBundleV1 should_override_builder: boolean ``` #### GetPayloadResponseV4 ```python class GetPayloadResponseV4(Container): execution_payload: ExecutionPayloadV3 block_value: uint256 blobs_bundle: BlobsBundleV1 should_override_builder: boolean execution_requests: List[ByteList[MAX_BYTES_PER_TRANSACTION], MAX_EXECUTION_REQUESTS] ``` #### GetPayloadResponseV5 ```python class GetPayloadResponseV5(Container): execution_payload: ExecutionPayloadV3 block_value: uint256 blobs_bundle: BlobsBundleV2 should_override_builder: boolean execution_requests: List[ByteList[MAX_BYTES_PER_TRANSACTION], MAX_EXECUTION_REQUESTS] ``` #### PayloadBodiesV1Response ```python class PayloadBodiesV1Response(Container): payload_bodies: List[List[ExecutionPayloadBodyV1, 1], MAX_PAYLOAD_BODIES_REQUEST] ``` *Note:* Each inner list has 0 elements for unknown blocks and 1 element for known blocks. #### GetBlobsV1Response ```python class GetBlobsV1Response(Container): blobs_and_proofs: List[BlobAndProofV1, MAX_BLOB_HASHES_REQUEST] ``` #### GetBlobsV2Response ```python class GetBlobsV2Response(Container): blobs_and_proofs: List[BlobAndProofV2, MAX_BLOB_HASHES_REQUEST] ``` #### GetBlobsV3Response ```python class GetBlobsV3Response(Container): blobs_and_proofs: List[List[BlobAndProofV2, 1], MAX_BLOB_HASHES_REQUEST] ``` *Note:* Each inner list has 0 elements for a missing blob and 1 element for a present blob. #### ClientVersionV1 ```python class ClientVersionV1(Container): code: ByteList[MAX_CLIENT_CODE_LENGTH] name: ByteList[MAX_CLIENT_NAME_LENGTH] version: ByteList[MAX_CLIENT_VERSION_LENGTH] commit: Bytes4 ``` #### GetClientVersionV1Response ```python class GetClientVersionV1Response(Container): versions: List[ClientVersionV1, MAX_CLIENT_VERSIONS] ``` ### Endpoints All endpoints use `Content-Type: application/octet-stream` for request and response bodies containing SSZ-encoded data. Error responses use `Content-Type: text/plain`. #### Payloads ##### `POST /engine/v{N}/payloads` — Submit execution payload Submit an execution payload for validation. The EL validates the payload and returns its status. | Version | Fork | Request Container | JSON-RPC Equivalent | | - | - | - | - | | v1 | Paris | `NewPayloadV1Request` | `engine_newPayloadV1` | | v2 | Shanghai | `NewPayloadV2Request` | `engine_newPayloadV2` | | v3 | Cancun | `NewPayloadV3Request` | `engine_newPayloadV3` | | v4 | Prague | `NewPayloadV4Request` | `engine_newPayloadV4` | **Request containers:** ```python class NewPayloadV1Request(Container): execution_payload: ExecutionPayloadV1 class NewPayloadV2Request(Container): execution_payload: ExecutionPayloadV2 class NewPayloadV3Request(Container): execution_payload: ExecutionPayloadV3 expected_blob_versioned_hashes: List[Bytes32, MAX_BLOB_COMMITMENTS_PER_BLOCK] parent_beacon_block_root: Bytes32 class NewPayloadV4Request(Container): execution_payload: ExecutionPayloadV3 expected_blob_versioned_hashes: List[Bytes32, MAX_BLOB_COMMITMENTS_PER_BLOCK] parent_beacon_block_root: Bytes32 execution_requests: List[ByteList[MAX_BYTES_PER_TRANSACTION], MAX_EXECUTION_REQUESTS] ``` **Response:** `200 OK` — `PayloadStatusV1` --- ##### `GET /engine/v{N}/payloads/{payload_id}` — Retrieve built payload Retrieve an execution payload previously requested via forkchoice update with payload attributes. The `{payload_id}` path parameter is the hex-encoded `Bytes8` payload identifier (e.g., `0x1234567890abcdef`). | Version | Fork | Response Type | JSON-RPC Equivalent | | - | - | - | - | | v1 | Paris | `ExecutionPayloadV1` | `engine_getPayloadV1` | | v2 | Shanghai | `GetPayloadResponseV2` | `engine_getPayloadV2` | | v3 | Cancun | `GetPayloadResponseV3` | `engine_getPayloadV3` | | v4 | Prague | `GetPayloadResponseV4` | `engine_getPayloadV4` | | v5 | Osaka | `GetPayloadResponseV5` | `engine_getPayloadV5` | **Request:** No body. The payload ID is in the URL path. **Response:** `200 OK` — SSZ-encoded response type from the table above. --- ##### `POST /engine/v{N}/payloads/bodies/by-hash` — Get payload bodies by hash Retrieve execution payload bodies for a list of block hashes. | Version | Fork | Response Type | JSON-RPC Equivalent | | - | - | - | - | | v1 | Shanghai | `PayloadBodiesV1Response` | `engine_getPayloadBodiesByHashV1` | **Request container:** ```python class GetPayloadBodiesByHashRequest(Container): block_hashes: List[Bytes32, MAX_PAYLOAD_BODIES_REQUEST] ``` **Response:** `200 OK` — `PayloadBodiesV1Response` --- ##### `POST /engine/v{N}/payloads/bodies/by-range` — Get payload bodies by range Retrieve execution payload bodies for a contiguous range of block numbers. | Version | Fork | Response Type | JSON-RPC Equivalent | | - | - | - | - | | v1 | Shanghai | `PayloadBodiesV1Response` | `engine_getPayloadBodiesByRangeV1` | **Request container:** ```python class GetPayloadBodiesByRangeRequest(Container): start: uint64 count: uint64 ``` **Response:** `200 OK` — `PayloadBodiesV1Response` #### Forkchoice ##### `POST /engine/v{N}/forkchoice` — Update fork choice Update the EL's fork choice state and optionally start building a new payload. | Version | Fork | Request Container | JSON-RPC Equivalent | | - | - | - | - | | v1 | Paris | `ForkchoiceUpdatedV1Request` | `engine_forkchoiceUpdatedV1` | | v2 | Shanghai | `ForkchoiceUpdatedV2Request` | `engine_forkchoiceUpdatedV2` | | v3 | Cancun | `ForkchoiceUpdatedV3Request` | `engine_forkchoiceUpdatedV3` | **Request containers:** ```python class ForkchoiceUpdatedV1Request(Container): forkchoice_state: ForkchoiceStateV1 payload_attributes: List[PayloadAttributesV1, 1] class ForkchoiceUpdatedV2Request(Container): forkchoice_state: ForkchoiceStateV1 payload_attributes: List[PayloadAttributesV2, 1] class ForkchoiceUpdatedV3Request(Container): forkchoice_state: ForkchoiceStateV1 payload_attributes: List[PayloadAttributesV3, 1] ``` **Response:** `200 OK` — `ForkchoiceUpdatedResponseV1` #### Blobs ##### `POST /engine/v{N}/blobs` — Get blobs by versioned hash Retrieve blobs from the EL's blob pool by their versioned hashes. | Version | Fork | Response Type | JSON-RPC Equivalent | | - | - | - | - | | v1 | Cancun | `GetBlobsV1Response` | `engine_getBlobsV1` | | v2 | Osaka | `GetBlobsV2Response` | `engine_getBlobsV2` | | v3 | Osaka | `GetBlobsV3Response` | `engine_getBlobsV3` | **Request container:** ```python class GetBlobsRequest(Container): blob_versioned_hashes: List[Bytes32, MAX_BLOB_HASHES_REQUEST] ``` **Response:** `200 OK` — SSZ-encoded response type from the table above, or `204 No Content` when the EL is syncing. #### Client ##### `POST /engine/v1/client/version` — Exchange client version Exchange client version information between CL and EL. **Request container:** ```python class GetClientVersionV1Request(Container): client_version: ClientVersionV1 ``` **Response:** `200 OK` — `GetClientVersionV1Response` #### Endpoint Summary | HTTP Method | Path | Fork | JSON-RPC Equivalent | | - | - | - | - | | `POST` | `/engine/v1/payloads` | Paris | `engine_newPayloadV1` | | `POST` | `/engine/v2/payloads` | Shanghai | `engine_newPayloadV2` | | `POST` | `/engine/v3/payloads` | Cancun | `engine_newPayloadV3` | | `POST` | `/engine/v4/payloads` | Prague | `engine_newPayloadV4` | | `GET` | `/engine/v1/payloads/{payload_id}` | Paris | `engine_getPayloadV1` | | `GET` | `/engine/v2/payloads/{payload_id}` | Shanghai | `engine_getPayloadV2` | | `GET` | `/engine/v3/payloads/{payload_id}` | Cancun | `engine_getPayloadV3` | | `GET` | `/engine/v4/payloads/{payload_id}` | Prague | `engine_getPayloadV4` | | `GET` | `/engine/v5/payloads/{payload_id}` | Osaka | `engine_getPayloadV5` | | `POST` | `/engine/v1/payloads/bodies/by-hash` | Shanghai | `engine_getPayloadBodiesByHashV1` | | `POST` | `/engine/v1/payloads/bodies/by-range` | Shanghai | `engine_getPayloadBodiesByRangeV1` | | `POST` | `/engine/v1/forkchoice` | Paris | `engine_forkchoiceUpdatedV1` | | `POST` | `/engine/v2/forkchoice` | Shanghai | `engine_forkchoiceUpdatedV2` | | `POST` | `/engine/v3/forkchoice` | Cancun | `engine_forkchoiceUpdatedV3` | | `POST` | `/engine/v1/blobs` | Cancun | `engine_getBlobsV1` | | `POST` | `/engine/v2/blobs` | Osaka | `engine_getBlobsV2` | | `POST` | `/engine/v3/blobs` | Osaka | `engine_getBlobsV3` | | `POST` | `/engine/v1/client/version` | All | `engine_getClientVersionV1` | ### Example #### Submit payload ```bash curl -X POST http://localhost:8551/engine/v4/payloads \ -H "Authorization: Bearer $JWT_TOKEN" \ -H "Content-Type: application/octet-stream" \ -H "Accept: application/octet-stream" \ --data-binary @new_payload_request.ssz \ -o payload_status.ssz ``` #### Retrieve built payload ```bash curl -X GET http://localhost:8551/engine/v4/payloads/0x1234567890abcdef \ -H "Authorization: Bearer $JWT_TOKEN" \ -H "Accept: application/octet-stream" \ -o get_payload_response.ssz ``` #### Update fork choice ```bash curl -X POST http://localhost:8551/engine/v3/forkchoice \ -H "Authorization: Bearer $JWT_TOKEN" \ -H "Content-Type: application/octet-stream" \ --data-binary @forkchoice_request.ssz \ -o forkchoice_response.ssz ``` ## Rationale ### Why REST instead of raw SSZ over TCP? REST is well understood, easy to debug, and infrastructure already exists for it (load balancers, proxies, monitoring). The Beacon API already uses REST with SSZ and it works well. Going lower level (raw TCP, custom framing) adds complexity without a clear benefit for the EL-CL link, which is typically localhost communication. ### Why same port instead of a separate port? Serving both JSON-RPC and SSZ REST on the same port simplifies deployment and configuration. There's no need for operators to manage an additional port, firewall rule, or JWT secret. The two transports coexist cleanly — JSON-RPC uses `POST /` with `Content-Type: application/json`, while SSZ REST uses resource-oriented paths with `Content-Type: application/octet-stream`. ### Why `application/octet-stream`? This is the standard content type for binary data. The Beacon API already uses it for SSZ responses. It signals that the body is raw bytes, not text, which is exactly what SSZ is. ### Why `text/plain` for errors instead of JSON? Errors are small, infrequent, and need to be human-readable for debugging. A plain text message body is the simplest possible approach — no parsing needed, just log it. JSON error bodies add complexity for minimal gain. ### No hard fork required This is purely a client-side change. It's a new transport option for an existing API — no consensus changes, no state transition changes, no new opcodes. Clients can implement it whenever they want and roll it out with a regular release. ## Backwards Compatibility This EIP introduces a new transport protocol alongside the existing JSON-RPC Engine API. The JSON-RPC API remains fully functional and is always available. Clients that don't implement binary SSZ are unaffected. ## Security Considerations - The SSZ REST transport uses the same JWT authentication as the JSON-RPC endpoint. All existing authentication requirements apply. - SSZ deserialization **MUST** enforce the same size limits as JSON deserialization. Implementations **MUST** reject SSZ payloads exceeding defined maximum sizes before attempting full deserialization. - Implementations **SHOULD** use well-tested SSZ libraries and fuzz test SSZ parsing extensively. - The `{payload_id}` path parameter **MUST** be validated as a well-formed hex-encoded `Bytes8` before processing. ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).