Skip to content

SSZ-REST Engine API Transport for Prysm#16447

Open
Giulio2002 wants to merge 28 commits into
OffchainLabs:developfrom
Giulio2002:eip-8161-ssz-rest
Open

SSZ-REST Engine API Transport for Prysm#16447
Giulio2002 wants to merge 28 commits into
OffchainLabs:developfrom
Giulio2002:eip-8161-ssz-rest

Conversation

@Giulio2002
Copy link
Copy Markdown
Contributor

@Giulio2002 Giulio2002 commented Mar 1, 2026

Implements SSZ-REST transport for the CL↔EL Engine API in Prysm, as specified in ethereum/execution-apis#764.

When connected to an EL that serves SSZ-REST endpoints on the Engine API port (/engine/v{N}/...), Prysm uses binary SSZ encoding instead of JSON-RPC for all Engine API calls — newPayload, forkchoiceUpdated, getPayload, getBlobs, and exchangeCapabilities.

What this PR does

  • SSZ-REST client (sszrest_client.go): HTTP client that encodes requests as SSZ (application/octet-stream) and sends them to REST endpoints on the engine port
  • SSZ encoding/decoding (sszrest_encoding.go, sszrest_types.go): SSZ container definitions for all Engine API request/response types, using List[T, 1] for nullable fields per spec
  • Transparent fallback: If any SSZ-REST call fails (network error, 4xx, encoding mismatch), Prysm falls back to JSON-RPC for that call — the chain always progresses
  • Same port: SSZ-REST is served on the existing engine port (default 8551) under /engine/* paths, no additional configuration needed
  • Fulu support: BlobsBundleV2 encoding for Fulu epoch and later
  • Dockerfiles for kurtosis devnet testing

Endpoints

Method Endpoint
new_payload POST /engine/v{N}/payloads
forkchoice_updated POST /engine/v3/forkchoice
get_payload GET /engine/v{N}/payloads/{payload_id}
get_blobs POST /engine/v1/blobs
exchange_capabilities POST /engine/v1/capabilities

Spec

  • Transport spec: ethereum/execution-apis#764
  • Benchmarks: SSZ encoding is 4–8× faster than JSON-RPC at 72 blobs, with 2× smaller wire size

Co-Authored-By: Claude noreply@anthropic.com

Implements the CL side of EIP-8161, enabling Prysm to communicate with
the EL via SSZ-REST instead of JSON-RPC when the EL advertises the
ssz_rest channel via EIP-8160.

- SSZ-REST client with automatic discovery via
  engine_getClientCommunicationChannelsV1
- SSZ encode/decode for all Engine API request/response types
- Transparent fallback to JSON-RPC on network errors
- Periodic channel refresh (every 5 minutes)
- --disable-ssz-rest flag to force JSON-RPC only
- Container-aware URL resolution (handles 0.0.0.0 advertised addresses)
- BlobsBundleV2 support for Fulu epoch and later
- Unit tests for SSZ encoding round-trips

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Mar 1, 2026

CLA assistant check
All committers have signed the CLA.

@Giulio2002 Giulio2002 changed the title [WIP] EIP-8161: SSZ-REST Engine API transport [WIP] Experiment Mar 1, 2026
@Giulio2002 Giulio2002 changed the title [WIP] Experiment [WIP] Big secret Mar 1, 2026
Call engine_exchangeCapabilitiesV2 first to get both capabilities and
supportedProtocols in a single request. Falls back to V1 + separate
GetClientCommunicationChannelsV1 if V2 is not supported.

This follows the updated EIP-8160 spec where protocol discovery is
integrated into the capability exchange handshake.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@potuz potuz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! is this something EL clients will be moving to? We'd be happy to move the engine calls to SSZ!

type sszRestClient struct {
baseURL string
httpClient *http.Client
mu sync.RWMutex
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this mutex even used? I couldn't find a guard and there seem to be plenty of races that could be preventing by using this very mutex. Looks like this was probably coded by an AI?

Copy link
Copy Markdown
Contributor Author

@Giulio2002 Giulio2002 Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually this is stateless and just a util so yes it was AI generated but the mu is not necessary so I removed it instead

Comment thread beacon-chain/execution/sszrest_client.go Outdated
Comment thread beacon-chain/execution/sszrest_client.go Outdated
Giulio2002 and others added 2 commits March 5, 2026 16:57
Remove getClientCommunicationChannels, exchangeCapabilitiesV2, and
protocol discovery. Replace --disable-ssz-rest with --ssz-rest-url flag
that directly specifies the EL's SSZ-REST endpoint. Add Dockerfile and
Dockerfile.validator for kurtosis devnet support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ding

Replace hand-written binary.LittleEndian offset encoding with
ssz.WriteOffset()/ssz.ReadOffset() from fastssz library for proper
SSZ container/list encoding in EIP-8161 ExchangeCapabilities.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Giulio2002 Giulio2002 marked this pull request as draft March 5, 2026 22:49
Giulio2002 and others added 7 commits March 6, 2026 00:01
…SSZ-REST

- new_payload → POST /engine/v{N}/payloads
- forkchoice_updated → POST /engine/v3/forkchoice
- get_payload → GET /engine/v{N}/payloads/{payload_id}
- get_blobs → POST /engine/v1/blobs
- exchange_capabilities → POST /engine/v1/capabilities
- get_client_version → POST /engine/v1/client/version
- Error responses now text/plain instead of JSON
- Added doGetRequest/doHTTP for GET support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Per execution-apis PR OffchainLabs#764 spec:
- PayloadStatus.latest_valid_hash: List[Hash32, 1]
- ForkchoiceUpdatedResponse.payload_id: List[Bytes8, 1]
- ForkchoiceUpdatedRequest.payload_attributes: List[PayloadAttributes, 1]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SSZ-REST URL is now derived from the engine endpoint (same host:port).
The EL serves SSZ-REST on the engine port under /engine/* paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously, only network-level errors (connection refused, timeout)
triggered JSON-RPC fallback. Protocol-level errors (400 bad request,
encoding mismatches) were returned directly, which could block
slot processing entirely. Now all SSZ-REST errors fall back to
JSON-RPC, ensuring the chain progresses even if SSZ encoding is wrong.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@Giulio2002 Giulio2002 changed the title [WIP] Big secret SSZ-REST Engine API Transport for Prysm Mar 8, 2026
@Giulio2002
Copy link
Copy Markdown
Contributor Author

Implements the SSZ-REST Engine API transport spec: ethereum/execution-apis#764

@Giulio2002 Giulio2002 marked this pull request as ready for review April 23, 2026 23:12
Giulio2002 and others added 2 commits April 24, 2026 01:51
Each engine method wrapped with SSZ-REST used to retry over JSON-RPC on
any error — including payload-status sentinels (SYNCING/ACCEPTED/INVALID)
which are valid engine responses, not transport failures. That doubled
the round-trip cost of every non-VALID response during sync and hid real
SSZ transport errors under a fallback.

Now: when SSZ-REST is active the SSZ call's result is propagated
directly — success, status sentinels, and transport errors all surface
as-is. JSON-RPC is kept only as the bootstrap path for the very first
exchange_capabilities, which fires before setupSSZRestClient runs.

Applies to NewPayload, ForkchoiceUpdated, GetPayload,
ExchangeCapabilities, GetBlobs, and GetClientVersionV1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@LukaszRozmej
Copy link
Copy Markdown

LukaszRozmej commented Apr 25, 2026

Nice! is this something EL clients will be moving to? We'd be happy to move the engine calls to SSZ!

Cooking, hope to have it done and merged in few weeks: NethermindEth/nethermind#11301

)

// MarshalSSZ ssz marshals the GetPayloadV2ResponseSSZ object
func (g *GetPayloadV2ResponseSSZ) MarshalSSZ() ([]byte, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how was this generated if no protobuf file was updated/updated

// through to the JSON-RPC path below — that's bootstrap, not fallback.
var elSupportedEndpointsSlice []string
if s.isSSZRestAvailable() {
result, err := s.exchangeCapabilitiesSSZRest(ctx, supportedEngineEndpoints)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a way to make these generic taking in a few different arguments just like call context?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what arguments do want to take? also not sure what generic you are talking about here

cfg := params.BeaconConfig()
var version int
switch {
case epoch >= cfg.GloasForkEpoch:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pretty sure we have a helper function for this already

Comment thread cmd/beacon-chain/main.go Outdated
var appFlags = []cli.Flag{
flags.DepositContractFlag,
flags.ExecutionEngineEndpoint,
flags.DisableSSZRouting,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usually we set the default to enable not disable, this is like the next step. we shouldn't have this enabled by default i think

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ops

Comment thread proto/engine/v1/sszrest_types.go Outdated
// status: uint8 (1 byte, fixed)
// latest_valid_hash: List[Hash32, 1] (variable — 0 or 32 bytes)
// validation_error: List[uint8, 1024] (variable — 0..1024 bytes)
type PayloadStatusV1SSZ struct {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These types I think should be protobuf types that generate the ssz.go file

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, got confused with other thing at the time

Comment thread beacon-chain/execution/service.go Outdated

// config defines a config struct for dependencies into the service.
type config struct {
disableSSZRouting bool
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need this ,we can add this as a feature flag instead, look at features.Get().EnableFullSSZDataLogging this probably changes the flag location too

Comment thread beacon-chain/execution/service.go Outdated
Comment thread proto/engine/v1/sszrest_getpayload.ssz.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants