---
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 <JWT token>
```

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