EIP-8178 - Binary SSZ Transport for the Engine API

Created 2026-03-01
Status Draft
Category Core
Type Standards Track
Authors

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

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

class WithdrawalV1(Container):
    index: uint64
    validator_index: uint64
    address: Bytes20
    amount: uint64

ExecutionPayloadV1

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.

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.

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.

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

class ForkchoiceStateV1(Container):
    head_block_hash: Bytes32
    safe_block_hash: Bytes32
    finalized_block_hash: Bytes32

PayloadAttributesV1

class PayloadAttributesV1(Container):
    timestamp: uint64
    prev_randao: Bytes32
    suggested_fee_recipient: Bytes20

PayloadAttributesV2

Extends PayloadAttributesV1 with withdrawals.

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.

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.

class ForkchoiceUpdatedResponseV1(Container):
    payload_status: PayloadStatusV1
    payload_id: Bytes8

Note: payload_id is all zeros when no payload building was initiated.

ExecutionPayloadBodyV1

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

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.

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

class BlobAndProofV1(Container):
    blob: ByteVector[BLOB_SIZE]
    proof: Bytes48

BlobAndProofV2

class BlobAndProofV2(Container):
    blob: ByteVector[BLOB_SIZE]
    proofs: List[Bytes48, CELLS_PER_EXT_BLOB]

TransitionConfigurationV1

Deprecated in Cancun.

class TransitionConfigurationV1(Container):
    terminal_total_difficulty: uint256
    terminal_block_hash: Bytes32
    terminal_block_number: uint64

GetPayloadResponseV2

class GetPayloadResponseV2(Container):
    execution_payload: ExecutionPayloadV2
    block_value: uint256

Note: Pre-Shanghai payloads have an empty withdrawals list.

GetPayloadResponseV3

class GetPayloadResponseV3(Container):
    execution_payload: ExecutionPayloadV3
    block_value: uint256
    blobs_bundle: BlobsBundleV1
    should_override_builder: boolean

GetPayloadResponseV4

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

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

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

class GetBlobsV1Response(Container):
    blobs_and_proofs: List[BlobAndProofV1, MAX_BLOB_HASHES_REQUEST]

GetBlobsV2Response

class GetBlobsV2Response(Container):
    blobs_and_proofs: List[BlobAndProofV2, MAX_BLOB_HASHES_REQUEST]

GetBlobsV3Response

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

class ClientVersionV1(Container):
    code: ByteList[MAX_CLIENT_CODE_LENGTH]
    name: ByteList[MAX_CLIENT_NAME_LENGTH]
    version: ByteList[MAX_CLIENT_VERSION_LENGTH]
    commit: Bytes4

GetClientVersionV1Response

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:

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 OKPayloadStatusV1


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:

class GetPayloadBodiesByHashRequest(Container):
    block_hashes: List[Bytes32, MAX_PAYLOAD_BODIES_REQUEST]

Response: 200 OKPayloadBodiesV1Response


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:

class GetPayloadBodiesByRangeRequest(Container):
    start: uint64
    count: uint64

Response: 200 OKPayloadBodiesV1Response

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:

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 OKForkchoiceUpdatedResponseV1

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:

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:

class GetClientVersionV1Request(Container):
    client_version: ClientVersionV1

Response: 200 OKGetClientVersionV1Response

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

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

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

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

Copyright

Copyright and related rights waived via CC0.