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.
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 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.
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.
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.
All endpoints are served under the /engine prefix on the existing Engine API port (default 8551):
http://localhost:8551/engine
| 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.
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.
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.
| Status | Meaning | Usage |
|---|---|---|
200 |
OK | Request succeeded, response body contains SSZ-encoded result |
204 |
No Content | Null result (e.g., syncing), empty body |
| 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 |
| Status | Meaning | Usage |
|---|---|---|
500 |
Internal Server Error | Unexpected server error |
Error responses use Content-Type: text/plain with a human-readable error message body.
| Name | Value | Source |
|---|---|---|
MAX_BYTES_PER_TRANSACTION |
2**30 (1,073,741,824) |
EIP-4844 |
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 |
BYTES_PER_FIELD_ELEMENT |
32 |
EIP-4844 |
CELLS_PER_EXT_BLOB |
128 |
EIP-7594 |
MAX_PAYLOAD_BODIES_REQUEST |
2**5 (32) |
Shanghai |
MAX_BLOB_HASHES_REQUEST |
128 |
Osaka |
MAX_EXECUTION_REQUESTS |
2**8 (256) |
EIP-7685 |
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 |
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.
class WithdrawalV1(Container):
index: uint64
validator_index: uint64
address: Bytes20
amount: uint64
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]
Extends ExecutionPayloadV1 with withdrawals.
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]
Extends ExecutionPayloadV2 with blob_gas_used and excess_blob_gas.
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
The status field is encoded as a uint8 enum.
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 |
class ForkchoiceStateV1(Container):
head_block_hash: Bytes32
safe_block_hash: Bytes32
finalized_block_hash: Bytes32
class PayloadAttributesV1(Container):
timestamp: uint64
prev_randao: Bytes32
suggested_fee_recipient: Bytes20
Extends PayloadAttributesV1 with withdrawals.
class PayloadAttributesV2(Container):
timestamp: uint64
prev_randao: Bytes32
suggested_fee_recipient: Bytes20
withdrawals: List[WithdrawalV1, MAX_WITHDRAWALS_PER_PAYLOAD]
Extends PayloadAttributesV2 with parent_beacon_block_root.
class PayloadAttributesV3(Container):
timestamp: uint64
prev_randao: Bytes32
suggested_fee_recipient: Bytes20
withdrawals: List[WithdrawalV1, MAX_WITHDRAWALS_PER_PAYLOAD]
parent_beacon_block_root: Bytes32
Used by all versions of engine_forkchoiceUpdated.
class ForkchoiceUpdatedResponseV1(Container):
payload_status: PayloadStatusV1
payload_id: Bytes8
Note: payload_id is all zeros when no payload building was initiated.
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.
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]
Proofs are cell proofs with CELLS_PER_EXT_BLOB proofs per blob.
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]
class BlobAndProofV1(Container):
blob: ByteVector[BLOB_SIZE]
proof: Bytes48
class BlobAndProofV2(Container):
blob: ByteVector[BLOB_SIZE]
proofs: List[Bytes48, CELLS_PER_EXT_BLOB]
Deprecated in Cancun.
class TransitionConfigurationV1(Container):
terminal_total_difficulty: uint256
terminal_block_hash: Bytes32
terminal_block_number: uint64
class GetPayloadResponseV2(Container):
execution_payload: ExecutionPayloadV2
block_value: uint256
Note: Pre-Shanghai payloads have an empty withdrawals list.
class GetPayloadResponseV3(Container):
execution_payload: ExecutionPayloadV3
block_value: uint256
blobs_bundle: BlobsBundleV1
should_override_builder: boolean
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]
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]
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.
class GetBlobsV1Response(Container):
blobs_and_proofs: List[BlobAndProofV1, MAX_BLOB_HASHES_REQUEST]
class GetBlobsV2Response(Container):
blobs_and_proofs: List[BlobAndProofV2, MAX_BLOB_HASHES_REQUEST]
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.
class ClientVersionV1(Container):
code: ByteList[MAX_CLIENT_CODE_LENGTH]
name: ByteList[MAX_CLIENT_NAME_LENGTH]
version: ByteList[MAX_CLIENT_VERSION_LENGTH]
commit: Bytes4
class GetClientVersionV1Response(Container):
versions: List[ClientVersionV1, MAX_CLIENT_VERSIONS]
All endpoints use Content-Type: application/octet-stream for request and response bodies containing SSZ-encoded data. Error responses use Content-Type: text/plain.
POST /engine/v{N}/payloads — Submit execution payloadSubmit 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:
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 payloadRetrieve 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 hashRetrieve execution payload bodies for a list of block hashes.
| Version | Fork | Response Type | JSON-RPC Equivalent |
|---|---|---|---|
| v1 | Shanghai | PayloadBodiesV1Response |
engine_getPayloadBodiesByHashV1 |
Request container:
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 rangeRetrieve execution payload bodies for a contiguous range of block numbers.
| Version | Fork | Response Type | JSON-RPC Equivalent |
|---|---|---|---|
| v1 | Shanghai | PayloadBodiesV1Response |
engine_getPayloadBodiesByRangeV1 |
Request container:
class GetPayloadBodiesByRangeRequest(Container):
start: uint64
count: uint64
Response: 200 OK — PayloadBodiesV1Response
POST /engine/v{N}/forkchoice — Update fork choiceUpdate 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:
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
POST /engine/v{N}/blobs — Get blobs by versioned hashRetrieve 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:
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.
POST /engine/v1/client/version — Exchange client versionExchange client version information between CL and EL.
Request container:
class GetClientVersionV1Request(Container):
client_version: ClientVersionV1
Response: 200 OK — GetClientVersionV1Response
| 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 |
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
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
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
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.
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.
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.
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.
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.
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.
{payload_id} path parameter MUST be validated as a well-formed hex-encoded Bytes8 before processing.Copyright and related rights waived via CC0.