feat(engine): implement Engine API v2 REST+SSZ surface per spec#11887
feat(engine): implement Engine API v2 REST+SSZ surface per spec#11887Dyslex7c wants to merge 41 commits into
Conversation
|
@claude review |
|
Claude finished @LukaszRozmej's task in 7m 35s —— View job PR Review: feat(engine): implement Engine API v2 REST+SSZ surface per spec
SummaryLarge, well-structured feature implementing the Engine API v2 REST+SSZ surface. Architecture is clean — the generic descriptor pattern ( Several issues need attention before merge. FindingsHigh1. Engine API port forced to HTTP/2 only — breaks existing HTTP/1.1 clients
2.
Medium3. Two
4. Empty bare Violates the repo robustness rule. The header-parsing catch should at minimum write a trace-level log so malformed payloads or bugs surface during debugging. 5. Hard-coded fork name list in
6. This is 7.
Low8.
9.
10.
What looks good
|
Deep Review — Engine API v2 REST+SSZ vs spec (execution-apis#793)I cross-checked the implementation against the authoritative spec (ethereum/execution-apis#793 — both Spec-conformance findings🟥 Blocking —
|
… worktree-pr-11887-review # Conflicts: # src/Nethermind/Nethermind.Merge.Plugin.Test/SszRest/SszMiddlewareTests.cs
ForkchoiceUpdatedSszHandler.GetUrlForkMismatchMessage compared
payloadSpec.Name to the URL fork segment. spec.Name is a display string
("bpo1", "Cancun NoBlobs", subclass variants), so a payload timestamped
inside a BPO fork sent to /engine/v2/osaka/forkchoice was being rejected
as unsupported-fork even though Osaka and BPO1 share the same engine API
surface. Add SszRestPaths.GetEngineApiUrlSegment to walk the parent chain
to the engine-API ancestor (BPO1 → Osaka, "Cancun NoBlobs" → Cancun) and
compare against that. Regression test
Forkchoice_payload_in_BPO_fork_routes_to_parent_url fails on the prior
spec.Name comparison.
Other findings from the same reviews:
- GetPayloadSszHandler: set Cache-Control: no-store before the
payload-id parse, so the header lands on the 400 path too — spec
applies it to all GET /payloads/{id} responses.
- EngineRpcCapabilitiesProvider: GET /engine/v2/blobs/v4 is annotated
// Osaka (EIP-7594/PeerDAS) and gated by IsEip7594Enabled; move it
into the Osaka block where it belongs.
- TryResolveHandler: drop the pathSegment.ToString() allocation; work
off ReadOnlyMemory<char>/ReadOnlySpan<char>, with a span overload of
MapForkToVersion. Now zero-alloc on the routing hot path.
- EncodeGetBlobsV4Response: guard against b.BlobCells / b.Proofs that
are shorter than CellsPerExtBlob (defensive; current callers are safe).
- GetBlobsHandlerV4: track GetBlobsRequestsFailureTotal on partial misses
for symmetry with GetBlobsHandler V1.
- EngineRpcCapabilitiesProvider: publish _ssz via Volatile.Write and read
via Volatile.Read for clarity (the implicit acquire through _jsonRpc
was correct but obscured the contract).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@claude re-review |
|
Claude finished @LukaszRozmej's task in 5m 17s —— View job PR Re-Review: feat(engine): implement Engine API v2 REST+SSZ surface per spec
SummaryAll blocking and high/medium issues from the June 12 LukaszRozmej deep review and the prior Claude re-review are resolved. The code is in a shippable state for non-BPO chains. Resolved since last review
Remaining findingsMedium
Changing Low
return span.Equals(capabilitiesPath.AsSpan(), StringComparison.OrdinalIgnoreCase)
|| span.Equals(identityPath.AsSpan(), StringComparison.OrdinalIgnoreCase);No test for BPO-fork FCU URL validation — What looks clean
|
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third deep review — cross-checked against execution-apis#793 (
|
S2 (validation_error wire shape): PayloadStatus.validation_error per
spec is Optional[String] = List[List[byte, 1024], 1]. PR encoded it as
List[byte, 1024] directly, missing the outer one-element-list layer.
Non-empty errors emitted 4 bytes less than spec. Wrap the bytes in a
new SszValidationError container and expose it via [SszList(1)].
S5 (bodies fork-range filtering): per spec, /engine/v2/{fork}/bodies/...
responses MUST mark entries available=false when the block timestamp
falls outside the URL fork's range. Apply the filter at the SSZ-REST
boundary (the underlying engine handler stays unscoped because it's
shared with JSON-RPC). New helper BodiesForkFilter resolves each entry's
engine-API URL segment via SszRestPaths.GetEngineApiUrlSegment and
zeroes out non-matching entries. By-hash uses the requested hashes for
lookup; by-range uses start+index. IBlockFinder bridged into the SSZ
DI container. Regression test
GetPayloadBodiesByHash_marks_out_of_fork_blocks_unavailable verifies a
Cancun-timestamped block at /shanghai/bodies/hash decodes as
available=false (and the test fails on the unfiltered handler).
S8 (payload.max_bytes): drop from 128 MiB to 64 MiB to match the
spec capabilities-response example. The previous inline comment
claimed alignment with a non-existent MAX_REQUEST_BODY_SIZE spec
constant; replace it with a reference to the actual spec text.
S1 (target_gas_limit) and S3 (BYTES_PER_CELL spec ambiguity) skipped
per author preference: target_gas_limit will land separately when the
block-builder side wires through, and BYTES_PER_CELL = 2048 stays
because it's the only value that interops with c-kzg. Both raised on
the upstream spec PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
S5 follow-up: the bodies engine handlers return PayloadBodiesV{1,2}DirectResponse
which is IDisposable (holds MemoryManager<byte> for BAL). The previous
S5 fix replaced the ResultWrapper with a new one wrapping the filtered
array, dropping the original wrapper on the floor — the pooled BAL
memory leaked. Forward disposal via wrapped.AddDisposable(result.Dispose)
so the using-block in WriteSszResultAsync chains through to the original.
S2 follow-up: SszValidationError wrapped its single variable field in a
Container header, adding 4 spurious bytes per non-empty error vs the
spec's flat List[List[byte, 1024], 1]. Same issue with NullableBlobCellWire
and NullableKzgProofWire — extra 4 bytes per cell entry. Switch all three
to [SszContainer(isCollectionItself: true)] so the generator elides the
container and emits the bare inner List per spec.
Verified by a regression test that asserts an INVALID PayloadStatus with
"bad" validation_error encodes to exactly 16 bytes (1 status + 8 offsets +
4 inner-list offset + 3 message bytes) — matches the spec wire layout
literally.
BodiesForkFilter: drop the now-unused urlFork-is-null defensive branch
(caller guards) and dedupe the indexer call per entry (saves an extra
PayloadBody.ToResult materialization, which is non-trivial — copies BAL).
Tests use Assert.EnterMultipleScope and Has.Length / Is.Empty per the
project's NUnit conventions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fourth deep review — issues found in the previous fix roundWhile auditing the S2/S5/S8 fixes I introduced two of my own bugs and one wire-shape mismatch that mirrors the pattern in S2. All three are now fixed in A. S5 introduced a pool-memory leakThe bodies engine handlers ( My S5 fix replaced the result = ResultWrapper<IReadOnlyList<TResult?>>.Success(
BodiesForkFilter.FilterByHash(result.Data, hashes, urlFork, blockFinder, specProvider));This drops the original wrapper on the stack. The new wrapper's Fix: chain the original disposal through B.
|
…_BYTES execution-apis NethermindEth#793 defines MAX_BAL_BYTES = MAX_BYTES_PER_TX = 2^30 and uses the same ByteList[MAX_BAL_BYTES] type on both block_access_list fields of ExecutionPayloadAmsterdam and ExecutionPayloadBodyAmsterdam. The PR had: SszExecutionPayloadV4.BlockAccessList: [SszList(0x0100_0000)] (16 MiB) ExecutionPayloadBodyV2Wire.BlockAccessList: [SszList(0x4000_0000)] (1 GiB) Internally inconsistent, and the 16 MiB cap on the payload side would reject spec-conformant CL submissions with a larger BAL even though the bodies side accepts them. Align SszExecutionPayloadV4 to MAX_BAL_BYTES = 2^30. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fifth deep review — wire-shape limit auditThis pass swept every Fixed
Spec
The PR had them inconsistent: SszExecutionPayloadV4.BlockAccessList: [SszList(0x0100_0000)] // 16 MiB
ExecutionPayloadBodyV2Wire.BlockAccessList: [SszList(0x4000_0000)] // 1 GiBThe 16 MiB cap on the payload-submission path is 64× smaller than spec and would reject spec-conformant Audit summary — all other
|
| Constant | Spec | PR site | Status |
|---|---|---|---|
MAX_BYTES_PER_TX |
2^30 | SszTransaction.Bytes |
✓ |
MAX_TXS_PER_PAYLOAD |
2^20 | all Transactions lists |
✓ |
MAX_WITHDRAWALS_PER_PAYLOAD |
16 | all Withdrawals lists |
✓ |
MAX_EXTRA_DATA_BYTES |
32 | SszExecutionPayloadV1.ExtraData |
✓ |
MAX_BLOB_COMMITMENTS_PER_BLOCK |
4096 | BlobsBundleV{1,2}Wire commitments/blobs |
✓ |
BYTES_PER_BLOB |
131072 | SszBlob.Bytes, BlobAndProofV{1,2}.Blob |
✓ |
CELLS_PER_EXT_BLOB |
128 | proofs/cells lists, indices bitvector | ✓ |
MAX_BLOB_COMMITMENTS_PER_BLOCK * CELLS_PER_EXT_BLOB |
524288 | BlobsBundleV2Wire.Proofs |
✓ |
MAX_VERSIONED_HASHES_PER_REQUEST / MAX_BLOBS_REQUEST |
128 | GetBlobsRequestWire, GetBlobsV4RequestWire |
✓ |
MAX_BODIES_REQUEST |
32 | GetPayloadBodiesByHashRequestWire, PayloadBodies*ResponseWire.Entries |
✓ |
MAX_EXECUTION_REQUESTS_PER_PAYLOAD |
256 | NewPayloadV{4,5}RequestWire.ExecutionRequests, GetPayloadResponseV{4,5,6}Wire.ExecutionRequests |
✓ |
MAX_ERROR_BYTES |
1024 | SszValidationError.Bytes |
✓ |
MAX_CLIENT_CODE_LENGTH |
2 | ClientVersionWire.Code |
✓ |
MAX_CLIENT_NAME_LENGTH / MAX_CLIENT_VERSION_LENGTH |
64 | ClientVersionWire.{Name,Version} |
✓ |
Bytes4 (commit) |
4 | ClientVersionWire.Commit |
✓ |
MAX_CLIENT_VERSIONS |
4 | GetClientVersionResponseWire.Versions |
✓ |
MAX_CAPABILITY_NAME_LENGTH |
64 | SszCapabilityName.Name |
✓ |
MAX_CAPABILITIES |
64 | both ExchangeCapabilities*Wire.Capabilities |
✓ |
Bitvector[CELLS_PER_EXT_BLOB] (16 bytes) |
128 bits | IndicesBitarray, SszCustodyColumns.Bits |
✓ |
| Payload-shape inheritance (V1 → V2 → V3 → V4) | per-fork additions match | spec chain | ✓ |
Containers also verified
ExecutionPayload{V1,V2,V3,V4}field ordering matches spec'sExecutionPayloadParis → Shanghai → Cancun → Prague → Osaka → Amsterdamprogression.Prague = CancunandOsaka = Prague = Cancunshape equivalences meanGetPayloadResponseV{4,5}Wire.ExecutionPayloadcorrectly usesSszExecutionPayloadV3;GetPayloadResponseV6Wirecorrectly usesSszExecutionPayloadV4.Optional[T] ≡ List[T, 1]convention now consistently expressed via[SszContainer(isCollectionItself: true)]wrappers (SszValidationError,NullableBlobCellWire,NullableKzgProofWire) — the spec wire layout forOptional[String]is byte-exact (regression test pins 16 bytes for"bad").BlobsBundleV1vsBlobsBundleV2shape difference matches spec (V1 one proof per blob, V2CELLS_PER_EXT_BLOBcell proofs per blob).SszNumericChecks.CheckedLongconverts SSZuint64→longwith overflow →InvalidDataException→ 400 ssz-decode-error. Clean.
Other checks
OptimismEngineRpcModulecomposesIEngineRpcModulerather than subclassing — engine_getBlobsV4 / forkchoiceUpdatedV4 reach the SSZ-REST surface through the underlying module without needing to be re-exposed on the Optimism JSON-RPC face. ✓TaikoEngineRpcModulector and base-call propagate the newgetBlobsHandlerV4parameter. ✓IBlockFinderis bound toIBlockTreeinBlockTreeModuleso the SSZ-REST DI bridge (services.Bridge<IBlockFinder>(ctx)) resolves at runtime. ✓- DI registrations of the bodies SSZ handlers (per descriptor version × body result type) match the handler constructor's
(IEngineRpcModule, IBlockFinder, ISpecProvider)signature. ✓ - Benchmark (
NewPayloadSerializationBenchmarks) dropsExpectedBlobVersionedHashesfrom the SSZ body to match the spec's removal of the field on NewPayload requests. ✓ PayloadAttributes.Validatefield-error reporting reorder makes the "missing field" error fire before the generic version-mismatch when attrs are below the timestamp-implied version. Sensible.
Tests
1522 pass after the BAL fix; the fix doesn't break any wire roundtrip — existing tests covered 16 MiB-sized BALs which still fit under the new (larger) 1 GiB cap.
Net
The PR's wire shapes are now (as far as I can verify) byte-equivalent to the spec for all containers I could check field-by-field against refactor-ssz.md. Issues still on the table from prior rounds:
- S1
target_gas_limit— deferred by author - S3
BYTES_PER_CELL— spec ambiguity, flagged upstream - S4 FCU response ACCEPTED restriction — encoder still permissive
- S6
BlobV3EntryWiredup ofBlobV2EntryWire— author unsure, raised on spec PR
CI red on Ethereum.Blockchain.Pyspec.Test [Sequential] — every Amsterdam fork test fails with "Unable to determine finalized state root at block 0". StatelessExecutor.InputDecoder calls NewPayloadRequest.Merkleize on the SSZ-encoded stateless input, and that root is used to look up the finalized state. The merkle-tree depth for variable lists is fixed by the [SszList(N)] limit (depth = ceil(log2(N/32))), so changing the BAL limit from 0x0100_0000 → 0x4000_0000 changes the V4 payload's merkle root and breaks the lookup against fixtures generated against the old limit. The execution-apis NethermindEth#793 sketch says MAX_BAL_BYTES = MAX_BYTES_PER_TX (2^30), but the pyspec test fixtures (and Nethermind's stateless executor input format that consumes them) were generated against the 16 MiB value. Keep 0x0100_0000 here until the upstream spec value is settled and the fixtures align; the divergence is already flagged on the spec PR (execution-apis#793#issuecomment-4695124899 / -4695633112). Re-fix later once spec + fixtures agree. ExecutionPayloadBodyV2Wire.BlockAccessList stays at 0x4000_0000 — bodies aren't merkleized by the stateless executor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI red — Pyspec stateless tests fixed by reverting BAL limitAll 16
Traced to NewPayloadRequest<TExecutionPayload>.Merkleize(input.NewPayloadRequest, out UInt256 root);
...
NewPayloadRequestRoot: new Hash256(root.ToLittleEndian())The merkle tree depth for an SSZ variable list is fixed by its Reverted in
The Tests still 1522 pass locally; CI re-running now. |
- remove dead NotFound field in GetBlobsHandlerV4 - remove unused test helpers (BuildPayloadBodiesByRangeRequest, BuildCapabilitiesRequest, BuildClientVersionRequest, EncodeToBytes) - correct ComputeCells docstring (262144 bytes, not 131072) - TODO: validate custodyColumns.Length when a consumer is wired - ClientVersionSszHandler now maps engine error codes via ErrorCodeToHttpStatus, matching the rest of the handler surface Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@claude re-review |
|
Claude finished @LukaszRozmej's task in 6m 15s —— View job PR Re-Review: feat(engine): implement Engine API v2 REST+SSZ surface per spec
SummaryThe most recent commit ( Three items from prior reviews remain open and are tracked with inline comments. Remaining FindingsMedium1. S1 — Spec MUST violation. A spec-conformant CL sending Amsterdam attributes with 2. S4 — The spec forbids this for 3. Breaking The Low4. S6 — The spec reuses What's been resolved since last review (
|
| Issue | Status |
|---|---|
SszBlobCell.BlobCellLength = 2048 (EIP-7594 correct value) |
✅ |
Pool-rented cell/proof arrays correctly sliced before FromSpan |
✅ |
Kestrel: non-engine ports back on Http1 + DisableAltSvcHeader |
✅ |
X-Engine-Client-Version parsing moved to ClientVersionSszHandler |
✅ |
ClientVersionSszHandler maps engine errors via ErrorCodeToHttpStatus |
✅ |
TryRoute / IsDiagnosticGetPath disagreement on /capabilities/foo |
✅ |
CapabilitiesSszHandler body built and cached once |
✅ |
GetBlobsHandlerV4 dead NotFound field removed |
✅ |
| Unused test helpers removed | ✅ |
BPO-aware fork routing via GetEngineApiUrlSegment parent-chain walk |
✅ |
GetPayloadSszHandler — Cache-Control: no-store on all responses |
✅ |
engine_getBlobsV4 gated under Osaka/IsEip7594Enabled |
✅ |
| Metrics: V4 blobs success/failure symmetry | ✅ |
SszValidationError / NullableBlobCellWire / NullableKzgProofWire — isCollectionItself: true |
✅ |
Bodies disposal leak (f9d3081) |
✅ |
BodiesForkFilter double-materialization fixed |
✅ |
| // Engine-API extension codes (-38xxx) | ||
| MergeErrorCodes.UnknownPayload => "/engine-api/errors/unknown-payload", | ||
| MergeErrorCodes.InvalidForkchoiceState => "/engine-api/errors/invalid-forkchoice", | ||
| MergeErrorCodes.InvalidPayloadAttributes => "/engine-api/errors/invalid-attributes", | ||
| MergeErrorCodes.TooLargeRequest => "/engine-api/errors/request-too-large", | ||
| MergeErrorCodes.UnsupportedFork => "/engine-api/errors/unsupported-fork", | ||
| MergeErrorCodes.ReorgTooDeep => "/engine-api/errors/reorg-too-deep", | ||
|
|
||
| // SSZ-REST-specific internal codes (-39xxx) | ||
| SszRestErrorCodes.SszDecodeError => "/engine-api/errors/ssz-decode-error", | ||
| SszRestErrorCodes.InvalidRequest => "/engine-api/errors/invalid-request", | ||
| SszRestErrorCodes.MethodNotFound => "/engine-api/errors/method-not-found", | ||
| SszRestErrorCodes.UnsupportedMediaType => "/engine-api/errors/unsupported-media-type", | ||
| SszRestErrorCodes.InvalidBody => "/engine-api/errors/invalid-body", |
There was a problem hiding this comment.
/engine-api/errors/parse-error is missing here. https://github.com/ethereum/execution-apis/pull/793/changes#diff-3a3573758f0adff7c117c2a01aa6a9010e5ab5d5045f514834e6a4b736153a6cR486
Summary
Implements the Nethermind execution-layer side of the Engine API v2 REST+SSZ spec proposed in ethereum/execution-apis#793. This supersedes execution-apis#764 which added SSZ encoding to the existing JSON-RPC surface; this PR takes the full step to a resource-oriented HTTP API. The new surface coexists with the existing JSON-RPC endpoint on port
8551— no breaking changes to existing behaviour. Fork negotiation is viaGET /engine/v2/capabilities.Architecture
Wire types (
SszWireTypes.cs) —[SszContainer]definitions for every Engine API structure across Paris → Amsterdam: per-forkExecutionPayload(V1–V4),PayloadAttributes(V1–V4),ExecutionPayloadBody(V1–V2),ForkchoiceUpdate,BuiltPayload, blob entry wrappers (BlobV1/V2/V3/V4EntryWire), body entry wrappers (BodyEntryV1/V2Wire),BlobCellsAndProofsWire,GetBlobsV4RequestWirewithBitvector[128]index selection, andSszPayloadIdwrappingByteVector[8]. Driven by the existingNethermind.Serialization.SszGeneratorsource generator.Optional[T]encoded asList[T, 1]throughout.Codec (
SszCodec.cs) — Stateless bridge between domain objects and wire types. Encoders write intoIBufferWriter<byte>, returning a byte count. Decoders acceptReadOnlySequence<byte>. Key removals:expectedBlobVersionedHashesdropped from allNewPayloadrequest wires — EL recomputes viaGetBlobVersionedHashes()which walkspayload.transactions.PayloadIdencoding changed fromuint64LE to opaqueByteVector[8](no byte-order flip).TargetGasLimitthreaded throughPayloadAttributesandBuildPayloadAttributes.Middleware (
SszMiddleware.cs) — ASP.NET Core middleware on the authenticated engine port. Routes under/engine/v2/{fork}/{resource}[/{extra}]for fork-scoped endpoints and/engine/v2/blobs/vN,/engine/v2/capabilities,/engine/v2/identityfor independently-versioned and unscoped endpoints. Distinguishes unrecognized fork segment (400 unsupported-fork) from unknown path (404 method-not-found). JWT auth,Content-Type: application/octet-streamenforcement (415), 128 MiB body cap. Non-SSZ requests pass through to JSON-RPC unchanged.Handler dispatch — Generic versioned handlers parameterised by per-version descriptor structs (
INewPayloadVersion<TWire>,IForkchoiceUpdatedVersion<TWire>,IGetPayloadVersion<TResult>,IPayloadBodiesByHashVersion,IPayloadBodiesByRangeVersion,IGetBlobsVersion). Adding a new fork is one descriptor struct and one DI line — no version switches anywhere. Every descriptor delegates toIEngineRpcModule.engine_*V{N}, so JSON-RPC and REST+SSZ share the same engine-module entry points.ForkchoiceUpdatedSszHandlervalidates fork/timestamp agreement againstISpecProvider; custody columns applied independently with errors swallowed per spec.Capabilities & identity (
CapabilitiesSszHandler.cs,ClientVersionSszHandler.cs) — Changed from POST to GET, both unscoped. Capabilities returns structured JSON (supported_forks,fork_scoped_endpoints,independently_versioned,limits) instead of delegating toengine_exchangeCapabilities. Identity readsX-Engine-Client-Versionrequest header and echoes the CL version in the response array.Error model (
SszRestErrorCodes.cs,SszEndpointHandlerBase.cs) — RFC 7807application/problem+jsonbodies with exactly two fields:type(required) anddetail(optional). Internal-39xxxsentinel codes disambiguate errors sharing an HTTP status (e.g. the three400subtypes).detailis suppressed for canned errors (ssz-decode-error,unauthorized). Numericstatusfield removed from body.Engine API extensions —
PayloadAttributesgainsTargetGasLimit(included in payload ID computation, required for V4 validation, surfaced viaGetGasLimit()).engine_getBlobsV4added withGetBlobsHandlerV4: validates request size, fetches blobs from tx pool, computes cells via newKzgPolynomialCommitments.ComputeCells(), returns only requested cells perIndicesBitarray.engine_forkchoiceUpdatedV4signature extended withBitArray? custodyColumns.MergeErrorCodesgainsUnsupportedFork = -38005andReorgTooDeep = -38006.DI bridge (
SszMiddlewareConfigurer.cs) —IJsonRpcServiceConfigurerthat bridges Autofac to ASP.NET MS DI; configures Kestrel withHttpProtocols.Http2on engine API ports only (non-engine ports keep HTTP/1.1) and setsINITIAL_WINDOW_SIZE/INITIAL_CONNECTION_WINDOW_SIZEto 1 MiB.Spec conformance
Aligned with ethereum/execution-apis#793:
400 ssz-decode-errorfor malformed SSZ bodies (canned, nodetail)400 invalid-requestfor malformed query parameters400 unsupported-forkfor unrecognized{fork}URL segment404 method-not-foundfor unmatched paths404 unknown-payloadfor expired or unknownpayloadId409 invalid-forkchoice/409 reorg-too-deepfor conflicting forkchoice state413 request-too-largepast advertisedlimits.*values415 unsupported-media-typefor wrongContent-Type/Accept422 invalid-attributesfor invalidpayload_attributes500 internalfor unrecoverable server errorsCache-Control: no-storeon allGET /payloads/{id}responsesexpectedBlobVersionedHashesremoved — EL recomputes frompayload.transactionsengine_exchangeTransitionConfigurationV1removed (deprecated since Cancun)Follow-up
traceparentpropagation (SHOULD) andzstdresponse compression (MAY; gzip already active) are deferred as both are optional per spec.Types of changes
What types of changes does your code introduce?
Testing
Requires testing
If yes, did you write tests?
Notes on testing
SszCodecTests— Encode/decode roundtrips updated to removeexpectedBlobVersionedHashesassertions; EL now recomputes versioned hashes from transactions.SszMiddlewareTests— Expanded to cover new/engine/v2/{fork}/...URL scheme, RFC 7807 error body shape (nostatusfield, correcttypeURI per error class),400 unsupported-forkvs404 method-not-foundfork routing, and GET query parameter validation for bodies-by-range.SszMultiSegmentDecodeTests— Updated to removeexpectedBlobVersionedHashesfrom multi-segment decode assertions.EngineModuleTests.V1/V6— UpdatedSszRestPathsarrays to match new URL scheme;engine_getBlobsV4added to expected capabilities;TargetGasLimitadded to allPayloadAttributesconstruction sites.Hive
enginetest suite run against/engine/v2/...endpoints still required.Documentation
Requires documentation update
Requires explanation in Release Notes
Nethermind now exposes the Engine API v2 REST+SSZ surface alongside the existing JSON-RPC engine API on port
8551. The new surface is available at/engine/v2/...and uses SSZ-encoded bodies over HTTP/2 (h2c). Consensus layer clients can discover supported forks, blob endpoint revisions, and per-endpoint request size limits viaGET /engine/v2/capabilities.