SSZ-REST Engine API Transport for Prysm#16447
Conversation
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>
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>
potuz
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
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>
…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>
|
Implements the SSZ-REST Engine API transport spec: ethereum/execution-apis#764 |
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>
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) { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
is there a way to make these generic taking in a few different arguments just like call context?
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
pretty sure we have a helper function for this already
| var appFlags = []cli.Flag{ | ||
| flags.DepositContractFlag, | ||
| flags.ExecutionEngineEndpoint, | ||
| flags.DisableSSZRouting, |
There was a problem hiding this comment.
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
| // 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 { |
There was a problem hiding this comment.
These types I think should be protobuf types that generate the ssz.go file
There was a problem hiding this comment.
sorry, got confused with other thing at the time
|
|
||
| // config defines a config struct for dependencies into the service. | ||
| type config struct { | ||
| disableSSZRouting bool |
There was a problem hiding this comment.
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
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, andexchangeCapabilities.What this PR does
sszrest_client.go): HTTP client that encodes requests as SSZ (application/octet-stream) and sends them to REST endpoints on the engine portsszrest_encoding.go,sszrest_types.go): SSZ container definitions for all Engine API request/response types, usingList[T, 1]for nullable fields per spec/engine/*paths, no additional configuration neededBlobsBundleV2encoding for Fulu epoch and laterEndpoints
new_payloadPOST /engine/v{N}/payloadsforkchoice_updatedPOST /engine/v3/forkchoiceget_payloadGET /engine/v{N}/payloads/{payload_id}get_blobsPOST /engine/v1/blobsexchange_capabilitiesPOST /engine/v1/capabilitiesSpec
Co-Authored-By: Claude noreply@anthropic.com