feat(engine): move SSZ-REST fork selection into Eth-Execution-Version header#12193
Conversation
… header
Aligns the SSZ-REST Engine API transport with the latest execution-apis#793
revision (commit d39e9a2, "move fork into header out of path"). Fork-scoped
endpoints (payloads, forkchoice, bodies) drop the {fork} URL segment and instead
negotiate the fork — and thus the SSZ container shape and engine method version —
via the Eth-Execution-Version request header. Blobs remain independently
path-versioned (/blobs/v1..v4) and identity/capabilities remain unscoped.
engine_exchangeCapabilities now advertises each distinct fork-less REST path
once; per-fork availability continues to be expressed via supported_forks in the
GET /engine/v2/capabilities body.
Missing header on a fork-scoped endpoint returns 400 invalid-request; an
unknown fork returns 400 unsupported-fork.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @LukaszRozmej's task in 5m 6s —— View job PR Review — SSZ-REST fork selection via
|
Addresses PR review: replace the duplicated "SszRouteFork" HttpContext.Items string literal (set in SszMiddleware, read in three handlers) with a single internal const, so a typo can't silently disable fork filtering. Also document why newPayload — unlike forkchoiceUpdated — skips an explicit header-vs-payload fork check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- A supported fork that predates a recognized endpoint (e.g. paris + bodies) now returns 400 unsupported-fork instead of 404 method-not-found; MapForkToVersion reports whether the resource was recognized so the middleware can tell "endpoint not on this fork" from "unknown endpoint". - Rename NamedReleaseSpec.EngineApiUrlSegment -> EngineApiForkName and SszRestPaths.GetEngineApiUrlSegment -> GetEngineApiForkName (plus urlFork -> requestedFork locals): the value is the Eth-Execution-Version header, not a URL. - Read the fork header via StringValues indexing rather than .ToString(), avoiding the per-request join and rejecting a 0- or multi-valued header as a bad request. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed the remaining three in 4f833d5:
Build clean; 115 SSZ-REST + capability tests pass. |
Cleanup from PR review: - Drop NamedReleaseSpec.EngineApiForkName (it was just Name?.ToLowerInvariant()); compute the lowercase fork name at the SSZ-REST use sites instead. - Replace the identity/capabilities/blobs special-cases in TryRoute with a data-driven SszRestPaths.GetScoping over the first path segment. - Rework MapForkToVersion to classify the endpoint (ClassifyMethod) then switch, dropping the recognizedResource-set-inline pattern. - Extract SszEndpointHandlerBase.GetRequestedFork(ctx) used by the fork-aware handlers instead of repeating the ctx.Items lookup. - Tests: drop the static readonly fork fields for lowercase literals and convert the object[] TestCaseSources to named TestCaseData. - Drop a few comments that restated the code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Its result is only ever compared case-insensitively (string.Equals OrdinalIgnoreCase and the OrdinalIgnoreCase _forkSpecByUrl lookup), so the per-block/per-FCU allocation was unnecessary — return the fork name as-is. The one-time lowercasing when building the supported-fork set stays, since those names are serialized verbatim into the lowercase supported_forks capabilities list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Replace the ForkScopedMethod enum + ClassifyMethod if-cascade + switch with two resource-keyed FrozenDictionary version-selector tables (POST/GET), looked up via cached span alternate-lookups. Presence encodes "recognized"; the selector yields the fork's method version (or null when the fork predates the endpoint). - Remove several comments that restated the code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| if (int.TryParse(sub[1..], out int blobVer)) | ||
| case SszRestPaths.ResourceScoping.Unscoped: | ||
| // No fork, no version, no extra segments (e.g. /capabilities, /identity). | ||
| if (!rest.IsEmpty) return false; |
There was a problem hiding this comment.
Minor / optional: the !rest.IsEmpty guard only rejects a non-empty trailing segment, so a trailing-slash-only path on an unscoped endpoint now resolves to a handler where it previously 404'd.
For GET /engine/v2/capabilities/: resource = capabilities, rest is empty (the trailing slash produces an empty remainder), the guard passes, pathSegment becomes capabilities/, and in TryResolveHandler extraMem ends up empty → the capabilities handler matches and returns 200. The pre-#793 code rejected /capabilities/ (and /identity/) via the explicit StartsWith("capabilities/") check.
Impact is cosmetic — no security or consensus effect — but it is inconsistent: /capabilities/foo still 404s while /capabilities/ now 200s. If strict rejection is intended, reject a trailing-slash-only remainder here (e.g. if (!rest.IsEmpty || slash >= 0) return false;) and pin it with a test; otherwise feel free to leave it as intentional trailing-slash tolerance.
Co-authored-by: Claude Opus 4.8 (1M context)
Per review: /engine/v2/capabilities/ and /identity/ previously resolved (200) while /capabilities/foo 404'd. Unscoped endpoints are exact fixed paths, so a trailing slash is now rejected too — capturing hadTrailingSlash before the collapse (the collapse otherwise hides it from the resource/rest split). Fork-scoped paths keep trailing-slash tolerance so a trailing slash never leaks into a payload id. Updated the trailing-slash test to assert 404 for both unscoped endpoints. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Fixed in 022496b. Unscoped endpoints now reject a trailing slash too, so One thing worth noting: your suggested |
|
Claude finished @LukaszRozmej's task in 3m 22s —— View job PR Review — SSZ-REST fork selection via
|
…dling Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…olveHandler Both now read ctx.Request.Method via case-insensitive HttpMethods.IsPost/IsGet. Previously MapForkToVersion used ordinal string.Equals, so a lowercase verb would pass TryResolveHandler's dict selection but fail here (spurious 404). No practical impact (Kestrel normalizes verbs) but removes the casing asymmetry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Good catch — agreed it's a latent footgun. Aligned in b26fce8: |
Changes
Aligns the SSZ-REST Engine API transport with the latest execution-apis#793 revision (commit
d39e9a2, "move fork into header out of path").payloads,forkchoice,bodies) drop the{fork}URL segment. The fork — and thus the SSZ container shape and engine method version — is now negotiated via theEth-Execution-Versionrequest header (e.g.Eth-Execution-Version: cancun).POST /engine/v2/payloads,POST /engine/v2/forkchoice,GET /engine/v2/payloads/{payload_id},POST /engine/v2/bodies/hash,GET /engine/v2/bodies./engine/v2/blobs/v1..v4) andidentity/capabilitiesremain unscoped, per spec.engine_exchangeCapabilitiesnow advertises each distinct fork-less REST path once (OR-ing enablement across the method versions that share it); per-fork availability continues to be expressed viasupported_forksin theGET /engine/v2/capabilitiesbody.Eth-Execution-Versionheader on a fork-scoped endpoint returns 400invalid-request; an unknown fork returns 400unsupported-fork.Notes for reviewers
The relevant spec section is still a working draft and does not pin the URL prefix, whether the header is echoed in responses, or the status codes for a missing/unsupported fork. The choices here — keeping the
/engine/v2/prefix and returning400— are pragmatic defaults and easy to revisit.SszRestPaths.GetEngineApiUrlSegment/NamedReleaseSpec.EngineApiUrlSegmentkeep their names although the value is now a header rather than a URL segment.Types of changes
What types of changes does your code introduce?
Testing
Requires testing
If yes, did you write tests?
Nethermind.Merge.Plugin.Test:SszMiddlewareTests(converted to header-based routing, with new missing-header and unknown-fork-in-header cases) and theSszRestPathsAreAdvertisedcoverage set were updated. 104 SSZ-REST tests and 23 capability tests pass; build is clean with no new warnings.Documentation
Requires documentation update