feat(engine): align SSZ-REST wire format with execution-apis#793 update#11998
Conversation
Tracks the 2026-06-14 spec amendment that resolved implementer feedback
on execution-apis#793. Wire-incompatible changes that required code:
* Paris BuiltPayload is now a wrapped `{payload, block_value}` container
rather than a bare ExecutionPayload. The SSZ V1 GetPayload descriptor
routes to engine_getPayloadV2 to obtain block_value; the JSON-RPC V1
surface is unchanged.
* Prague+ BuiltPayload now puts `execution_requests` before
`should_override_builder`, diverging from the legacy JSON-RPC envelope
order. Reordered V4/V5/V6 wire structs accordingly.
* /v3 reuses BlobV2Entry verbatim; the spec now explicitly forbids a
separate BlobV3Entry wire type. Removed BlobV3EntryWire and
GetBlobsV3ResponseWire; EncodeGetBlobsV3Response aliases V2.
* Trailing slashes are no longer rejected — the spec dropped the
prohibition. SszMiddleware now strips a single trailing slash before
routing.
Already aligned with the updated spec, no code change required:
BYTES_PER_CELL=2048, MAX_REQUEST_BODY_SIZE=64 MiB, bodies.max_count=32,
HTTP/1.1+HTTP/2 negotiation on the engine port, single-field container
wrappers on bodies/blobs requests/responses, range-query truncation at
head, custody_columns Optional on Amsterdam ForkchoiceUpdate.
Open spec question deferred: BuiltPayloadShanghai literally includes
`should_override_builder` per the spec text, but this is being
questioned on execution-apis#793 as a possible typo. Left V2 wire
unchanged pending resolution.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Claude finished @LukaszRozmej's task in 6m 18s —— View job PR Review: feat(engine): align SSZ-REST wire format with execution-apis#793 updateTasks:
SummaryThe PR faithfully tracks the 2026-06-14 amendment to execution-apis#793. Each of the four wire-breaking changes is correctly implemented and tested:
The open Shanghai/ FindingsMediumTrailing slash fix is incomplete for fork-scoped
The spec says trailing slashes are no longer forbidden, so this is spec non-conformant. In practice, well-implemented CLs rarely add a trailing slash to an ID-bearing URL, but the inconsistency is worth fixing. Simplest fix: strip the trailing slash from LowInitializer order in V4/V5/V6 encode methods ( No dedicated offset tests for V5/V6 wire types: VerdictNot mergeable due to the Medium finding (incomplete trailing slash handling). The fix is small and isolated to |
| return false; | ||
| // execution-apis#793 no longer forbids trailing slashes — collapse them so the route resolves. | ||
| if (span.Length > EnginePrefix.Length && span[^1] == '/') | ||
| span = span[..^1]; |
There was a problem hiding this comment.
Medium – trailing slash handling is incomplete for fork-scoped {id} paths.
The span is trimmed here, but pathSegment is subsequently computed from the original path string, not from the trimmed span. This means the trailing slash survives into extra for any fork-scoped endpoint that carries a path extra, e.g.:
GET /engine/v2/paris/payloads/0x0102030405060708/
Trace for that URL:
spanafter trim →paris/payloads/0x0102030405060708offset→ 18 (unchanged)pathSegment = path.AsMemory(18)→"payloads/0x0102030405060708/"(trailing slash from originalpath)TryResolveHandler:extraMem = "0x0102030405060708/"→ ID parsing fails → 400
Only simple endpoints without a path extra (i.e. /capabilities/, /identity/) are actually fixed by this change. The spec says trailing slashes are no longer forbidden, so this is spec non-conformant for ID-bearing GET endpoints.
Simplest fix — strip from the pathSegment before returning it:
ReadOnlyMemory<char> ps = path.AsMemory(offset);
if (!ps.IsEmpty && ps.Span[^1] == '/')
ps = ps[..^1];
pathSegment = ps;Or normalize the path string at the top of the method and work from that everywhere.
Drops the V1↔V2 naming confusion: the Paris SSZ surface wraps a BuiltPayloadParis container, so renames GetPayloadResponseV1Wire -> BuiltPayloadParisWire EncodeGetPayloadV1Response -> EncodeBuiltPayloadParis The descriptor stays GetPayloadDescriptorV1 (it identifies the Engine API routing version) but now calls the encoder by its spec-level name. Test method names follow. Also strips inline `execution-apis#793` references that read more as PR-changelog than code rationale. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The descriptor now returns GetPayloadV2Result (V1 SSZ surface wraps
{payload, block_value}); the benchmark host registration still passed
ExecutionPayload as TResult, breaking the debug Benchmarks build.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…span TryRoute trimmed the slash from the local span used for matching but still sliced `pathSegment` from the original `path`, so for fork-scoped endpoints carrying a path id (e.g. /paris/payloads/0x.../) the slash leaked into `extra` and the payload-ID hex parser rejected it with 400. Slice every `path.AsMemory(...)` against the trimmed length so the route is fully canonicalised before dispatch. Adds a regression test on the V1 GetPayload route. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@claude re-review |
|
Claude finished @LukaszRozmej's task in 4m 13s —— View job PR Re-Review: feat(engine): align SSZ-REST wire format with execution-apis#793 updateTasks:
SummaryThe previous Medium finding (trailing slash leaking into Previous Medium Finding — FIXED ✅
The old approach ( int pathLen = path.Length;
if (pathLen > EnginePrefix.Length && path[pathLen - 1] == '/')
{
pathLen--;
span = span[..pathLen];
}
// ...
pathSegment = path.AsMemory(offset, pathLen - offset); // correctly excludes trailing slashBoth the unscoped (
Remaining FindingsLowBenchmark round-trip URL is stale ( The Initializer order in The object initializers list No offset tests for V5/V6 (pre-existing)
VerdictMergeable. The Medium blocking finding from the previous review is correctly fixed. Remaining issues are Low/pre-existing and do not warrant blocking merge. |
Address the review's Low findings:
* `EncodeGetPayload{V4,V5,V6}Response` initializers now list
`ExecutionRequests` before `ShouldOverrideBuilder`, matching the
struct field declaration order. SSZ serialisation order is governed
by the declaration, not the initializer, so this is purely a
readability fix — but it removes the visual mismatch when verifying
spec compliance.
* Added `EncodeGetPayloadV{5,6}Response_all_static_fields_land_at_spec_defined_offsets`
paralleling the existing V4 offset test. Shared assertion helper
`AssertPragueBuiltPayloadOffsets` since the 45-byte fixed section is
identical across V4/V5/V6 (BlobsBundleV{1,2} and ExecutionPayloadV{3,4}
are all variable-size).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@claude re-review |
|
Claude finished @LukaszRozmej's task in 2m 19s —— View job PR Re-Review: feat(engine): align SSZ-REST wire format with execution-apis#793 updateTasks:
SummaryAll four wire-breaking changes mandated by the 2026-06-14 amendment to execution-apis#793 are correctly implemented and tested. The trailing-slash Medium finding from the first review is fixed. The V5/V6 offset-test gap noted as Low in the second review has also been filled. FindingsAll previous Medium findings — FIXED ✅Trailing slash fix (commit Spec correctness checklist
Low (pre-existing, not blocking)Benchmark round-trip URL ( Initializer order in VerdictMergeable. No Critical, High, or Medium findings. All spec changes from the 2026-06-14 amendment are correctly implemented, the trailing-slash Medium finding is resolved, and the previously-noted V5/V6 offset-test gap has been filled. |
Summary
Tracks the 2026-06-17 update to ethereum/execution-apis#793 (commit
e509f20f— advertise all forks), which supersedes the 2026-06-14 commit83151eea— engine-api: resolve feedback from implementers this PR originally targeted. Follow-up to #11887.The 2026-06-17 spec update is largely documentation — per-fork worked examples for every endpoint, an explicit normative statement that the
{fork}URL segment spans Paris through Amsterdam, and a clarification that BPO forks do not introduce their own{fork}segment (they reuse the named fork they layer on). Two items affect implementations:BuiltPayloadShanghairesolved as{payload, block_value}. The spec author confirmed in 2026-06-17 that the Shanghai wrapper does NOT carryshould_override_builder— the field was introduced in Cancun (engine_getPayloadV3), alongsideblobs_bundle. Nethermind already shipped the Shanghai shape this way in the previous commit on this PR (V2 wire ={payload, block_value}, intentionally diverging from the as-written-but-flagged-as-typo 2026-06-14 text), so no further code change is needed.BlobsBundleV{1,2}list caps renamed. The spec now usesMAX_BLOB_COMMITMENTS_PER_BLOCK(4096) in place ofMAX_BLOBS_PER_PAYLOAD. Both resolve to the same numeric cap (4096), so the wire format is unchanged — only the spec naming was clarified. Nethermind already encodes with the 4096 cap.Everything else the 2026-06-17 commit codifies — fork-segment range, BPO routing to the parent named fork, the
400 /engine-api/errors/unsupported-forkresponse — already matched the existing implementation. No wire or behavioral changes vs. the previous commit on this PR.The earlier (2026-06-14) wire-incompatible changes this PR ships, unchanged from the previous commit:
Paris
BuiltPayloadis now wrapped.BuiltPayloadParis = { payload, block_value }per spec — was a bareExecutionPayloadParisbefore. AddedBuiltPayloadParisWire;GetPayloadDescriptorV1routes toengine_getPayloadV2to sourceblock_value. The JSON-RPC V1 surface is unchanged — only the SSZ-REST/paris/payloads/{id}response is now wrapped.Prague+ field order swapped. Per the spec's now-explicit normative ordering,
execution_requestsprecedesshould_override_builderon the wire — intentionally diverging from the legacy JSON-RPCengine_getPayloadenvelope order. ReorderedGetPayloadResponseV{4,5,6}Wire./v3reusesBlobV2Entryverbatim. The spec now explicitly forbids defining a separateBlobV3Entrywire type (to avoid the two drifting apart). RemovedBlobV3EntryWireandGetBlobsV3ResponseWire;EncodeGetBlobsV3Responsealiases the V2 encoder.Trailing slashes no longer rejected. The spec dropped the Trailing slashes are forbidden sentence.
SszMiddleware.TryRoutenow strips a single trailing slash before routing, so/engine/v2/capabilitiesand/engine/v2/capabilities/resolve identically.Types of changes
The SSZ-REST wire format changes here are breaking against the surface introduced in #11887, but that surface is itself unreleased and the spec it targets is still draft. No JSON-RPC behaviour changes.
Testing
Notes on testing
SszCodecTests— V1 GetPayload assertions updated for the new wrapper offsets (payload now lives at offset 36 inside the outer container; outer carries the 4-byte payload offset + 32-byte block_value). V4 assertions adjusted for the swappedexecution_requests/should_override_builderorder (should-override-builder moves from offset 40 to 44, execution-requests offset from 41 to 40).SszMiddlewareTests— Removed two trailing-slash 404 cases that are no longer applicable; added a positiveTrailing_slash_on_capabilities_resolves_normallycase. V1 routing test updated for the V2-source change.Nethermind.Merge.Plugin.Testtests pass; 107 SSZ-specific tests pass.Hive
enginetest suite run against/engine/v2/...endpoints still required.Documentation
🤖 Generated with Claude Code