feat(merge): add SSZ-REST transport for Engine-API#11301
feat(merge): add SSZ-REST transport for Engine-API#11301Dyslex7c wants to merge 72 commits intoNethermindEth:masterfrom
Conversation
|
@claude review |
|
Claude finished @LukaszRozmej's task in 4m 10s —— View job PR Review: feat(merge): add SSZ-REST transport for Engine-APITasks
SummaryGood foundation — the middleware architecture, route dispatch, and SSZ codec coverage are well-structured. Several issues need to be addressed before merge: High1. Memory exhaustion / DoS in
// Current – dangerous with chunked bodies
using MemoryStream ms = new();
await ctx.Request.Body.CopyToAsync(ms); // unbounded growth here
if (ms.Length > MaxBodySize) { throw … }Fix: pre-allocate the stream at the limit and abort early if the data overflows, or use using MemoryStream ms = new(MaxBodySize + 1);
byte[] buffer = ArrayPool<byte>.Shared.Rent(81920);
try {
int read;
while ((read = await ctx.Request.Body.ReadAsync(buffer, ctx.RequestAborted)) > 0) {
if (ms.Length + read > MaxBodySize)
throw new InvalidOperationException($"Request body exceeds {MaxBodySize} bytes");
ms.Write(buffer, 0, read);
}
} finally { ArrayPool<byte>.Shared.Return(buffer); }2. The regex captures // Current
int version = int.Parse(match.Groups["version"].Value); // OverflowException → 500
// Fix
if (!int.TryParse(match.Groups["version"].Value, out int version))
{
await WriteErrorAsync(ctx, StatusCodes.Status404NotFound, $"Invalid version in path: {path}");
return;
}Medium3. Internal exception messages leaked to callers (SszMiddleware.cs:130–132) catch (Exception ex)
{
if (_logger.IsError) _logger.Error($"SSZ-REST handler error for {path}", ex);
await WriteErrorAsync(ctx, StatusCodes.Status500InternalServerError, ex.Message);
}
await WriteErrorAsync(ctx, StatusCodes.Status500InternalServerError, "Internal server error");4. Missing Per await ctx.Request.Body.CopyToAsync(ms); // should use ctx.RequestAborted
await ctx.Response.Body.WriteAsync(data); // should use ctx.RequestAbortedA client disconnect leaves the body-read in flight indefinitely. 5. No graceful shutdown check The JSON-RPC fast lane in 6.
Please add a comment citing the relevant section of ethereum/execution-apis#764 that defines this split, or fix the mapping so the same version number is consistent in both directions. Without the spec reference it's impossible for reviewers to verify this is intentional. Low7. Indentation inconsistency in SszCodec.cs (lines 495–656) Private methods 8. Duplicated pattern match for BlobGasUsed = ep is ExecutionPayloadV3 v3 ? v3.BlobGasUsed ?? 0 : 0,
ExcessBlobGas = ep is ExecutionPayloadV3 v3b ? v3b.ExcessBlobGas ?? 0 : 0 // separate match, different var nameExtract once: 9.
10. User-supplied path echoed in 404 error bodies (SszMiddleware.cs:103) await WriteErrorAsync(ctx, StatusCodes.Status404NotFound, $"Unknown SSZ endpoint: {path}");Harmless in plain text ( 11.
Code coverage gaps
|
|
@claude re-review |
|
Claude finished @LukaszRozmej's task in 8m 0s —— View job PR Re-Review: feat(merge): add SSZ-REST transport for Engine-APITasks
What Was Fixed Since the Last ReviewAll six High issues from the previous review are resolved:
All five test-coverage gaps from the previous review are now covered: 401 auth failure, 413 oversized body, 404 unknown path, malformed SSZ → 500, 204 null-blob path.
Remaining IssuesHigh 1.
When a CL sends The fix is to dispatch to version-specific payload-attributes wire types in 2.
A separate V1 wire type (or suppressing Medium 3. The method sets 4. Still 64 bytes; unaddressed from the prior review. 128 bytes recommended with a spec citation. Low 5. LINQ
6. Leftover commented-out using directive (inline comment)
7. User-supplied path echoed in 404 error body (
SummaryGood progress — all previous High findings are resolved, and the new tests are a meaningful improvement. The two new High issues relate to SSZ wire-format mismatches for older versioned endpoints ( |
|
@claude re-review |
|
Claude finished @LukaszRozmej's task in 9m 17s —— View job PR Re-Review: feat(merge): add SSZ-REST transport for Engine-APITasks
What Was Fixed Since the Last ReviewAll findings from the previous re-review are addressed:
Remaining IssuesCritical 1. Field ordering mismatch in SSZ payload wrappers breaks wire compatibility (inline comment)
The Ethereum consensus spec (Bellatrix+) specifies: The code produces:
Fix: in both Medium 2.
3. Round-trip tests cannot catch field ordering bugs (inline comment) Every codec test encodes then decodes the same struct, so they pass even when the wire layout is wrong (because both sides use the same wrong layout). The Critical field ordering bug above was introduced by SummaryThe |
…ng literal Remove the duplicate SszIgnoreAttribute declaration from the generator's local Attributes.cs and replace `typeof(SszIgnoreAttribute).FullName` with a const string `"Nethermind.Serialization.Ssz.SszIgnoreAttribute"` directly. The analyzer compares against the resolved attribute symbol's `ToDisplayString()`, which is name-based regardless of which CLR type backs the comparison. Adding a generator-local attribute class just to make `typeof(...)` compile was machinery in service of a single string constant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the ArrayPoolSpan<byte> intermediate with `Func<T, IBufferWriter<byte>, int>` encoders that GetSpan/Encode/Advance straight into ctx.Response.BodyWriter, then set ContentLength from the returned byte count before flushing. Saves a pool rent + copy per response. Streaming-decode (read-side rewrite) is the next PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ract Encoders write directly into the response PipeWriter; if encode throws after one or more Advance calls, the partial bytes can't be rewound. The middleware's outer catch then writes a text error into the same pipe, producing a 500 with [partial-SSZ][text] body. Wrap encode in a try/catch that calls ctx.Abort() so the CL sees a connection reset instead of a garbled response. Also Debug.Assert that the encoder's reported length matches pipe.UnflushedBytes delta — catches drift between T.GetLength and T.Encode in test builds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Middleware now uses PipeReader.ReadAtLeastAsync(ContentLength) and passes the resulting ReadOnlySequence<byte> through the handler chain. Generator emits a Decode(ReadOnlySequence<byte>, out T) overload at every container/list/union emission site: single-segment dispatches to span (zero copy); multi-segment consolidates once via ArrayPool. Eliminates the prior MemoryStream + ToArray intermediate copy. SszLib gains primitive sequence overloads as infrastructure for a future fully recursive sequence-aware decoder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Existing SszCodecTests wrap byte[] via the single-segment ReadOnlySequence constructor — exercises only the IsSingleSegment fast path. Production traffic from Kestrel arrives in 4 KB pooled blocks, so blob-bearing NewPayload always runs through the generator's pool-rent + CopyTo branch and SszLib's stack-copy fallback. New fixture builds explicit multi-segment sequences via a ReadOnlySequenceSegment chain at boundary sizes 1/3/7/4096, covering the wire roundtrip path (TransitionConfig, Capabilities, GetBlobs, BodiesByRange, NewPayloadV3) and the SszLib primitive boundary path (uint, ulong, UInt256). Also restructures SszMiddleware.InvokeAsync as a chained if/else if/else with the body-handling work nested in the final else clause so handler/version/extra stay in scope without scattering early returns. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After merging master, IEngineRpcModule.engine_getBlobsV{1,2,3} returns
IReadOnlyList<BlobAndProof?> (the V{2,3} variant additionally nullable on the
outer wrapper) instead of IEnumerable<BlobAndProof>. Update the NSubstitute
returns in StartupTests and turn on #nullable enable so the annotations parse.
Also: master tightened Decode(ReadOnlySpan<byte>, out bool) to throw on bytes
other than 0/1. Route the sequence overload through ToContiguous so it can't
silently disagree with the span path on invalid input.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round-14 micro-optimizations.
Routing slow path:
* Build _postPrefixRoutes / _getPrefixRoutes from only resources that have at
least one AcceptsPathExtra candidate, and only those candidates. Today no
POST handler accepts a path-extra suffix, so _postPrefixRoutes is empty —
any failed POST exact-match no longer scans dead routes.
* Drop the redundant pathSpan.Equals(resourceSpan, ...) check in the prefix
loop (the next length check rules out exact-match) and the per-candidate
AcceptsPathExtra filter (now enforced at construction).
EncodeGetBlobsV{1,2}Response:
* Two-pass count + single exact-size allocation, replacing the previous
alloc-full-then-slice-trim pattern. One heap allocation per response
instead of two when any null entries are dropped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@claude review |
|
Claude finished @LukaszRozmej's task in 8m 42s —— View job Round 12 Review — SSZ-REST Engine API TransportPrior findings: all resolved ✅Round 11 Mediums
Round 10 Lows
Fresh pass on recent commitsThree perf-focused commits landed since Round 11. No new issues found.
The encode-to-pipe flow is correct:
Lifecycle is well-managed:
Prefix-route pre-filtering (
SummaryNo new Critical, High, or Medium findings. No remaining open findings from any prior round. The PR is ready to merge. Counts: Critical 0 · High 0 · Medium 0 · Low 0 |
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Capability tables are built once on first GetXxx call and never mutated. Switch the internal storage and the IRpcCapabilitiesProvider return type to FrozenDictionary<string, RpcCapabilityOptions> — exposes that immutability in the API and lets callers use FrozenDictionary-specific lookups (e.g. AlternateLookup<ReadOnlySpan<char>>) without re-freezing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If GetTypeByMetadataName for SszBasicTypeAttribute returns null (e.g. during a parallel-build race where Nethermind.Serialization.Ssz reference resolution lags), the previous gate cached the failure. Subsequent emits in the same compilation then short-circuited via the gate with KnownTypes empty and silently treated [SszBasicType] types like SszBytes8 as plain containers, producing references to nonexistent SszBytes8.GetLength/Encode/Decode/MerkleizeList methods. Only short-circuit when prior discovery actually populated KnownTypes; never cache the empty/null result. A re-walked namespace tree is far cheaper than a build that fails with confusing CS0117s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes NethermindEth#11525. Replace the [SszContainer]+byte[] shape of SszKzgCommitment with an [InlineArray(48)] basic-type struct (mirrors SszBytes8). SszKzgCommitment[] is now a single contiguous count*48-byte heap allocation with no inter-element references — better cache locality during encode and removes the per-element pointer chase the GC otherwise marks through. Wire format is identical (raw 48 bytes per element); the existing SSZ generator already supports basic ref-types with custom encode/decode templates, so no generator changes were needed. SszBlob (131,072) stays on byte[] per the issue's explicit non-conversion clause. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@claude what do you think about the following?
Files: SszWireTypes.cs:312-322 [SszContainer] SszCapabilityName { [SszList(64)] byte[]? Name } is a Container with one variable field, defaulting to isCollectionItself: false. Verified against SszGenerator.cs:729-988: that path emits [uint32 offset = 4][bytes] per element. The isCollectionItself flag exists exactly to skip that wrapper (used on SszTransaction). Spec (ssz-encoding.md:570) wants List[ByteList[64], 64] — raw bytes, no per-element container prefix. Fix: add (isCollectionItself: true) on SszCapabilityName. Apply the same to ExchangeCapabilitiesRequestWire.
Files: SszRestPaths.cs:16,29, EngineRpcCapabilitiesProvider.cs:83, TransitionConfigurationSszHandler.cs, SszCodec.cs:215-236, SszWireTypes.cs:438-451 The spec endpoint summary (ssz-encoding.md:899-924) lists every SSZ endpoint; transition-configuration is absent — the spec author confirmed deletion in commit aca7eb6. The implementation still serves the path, advertises it in engine_exchangeCapabilities, and carries dead wire types and a handler. Fix: delete the handler, wire types, capability registration, and SszRestPaths constants. The JSON-RPC engine_exchangeTransitionConfigurationV1 can stay for legacy CLs.
Files: SszCodec.cs:22-29, 290-298 SszPayloadStatus.InvalidBlockHash = 4 and the EngineStatusToSsz switch maps PayloadStatus.InvalidBlockHash → 4. Spec (ssz-encoding.md:334-340) defines exactly four values (VALID/INVALID/SYNCING/ACCEPTED); the spec author dropped INVALID_BLOCK_HASH per Lukasz's review thread. The engine's V1 newPayload path can still return InvalidBlockHash (Paris semantics). Fix: in the V1 SSZ encode path, normalise InvalidBlockHash → Invalid. Drop the constant.
File: SszMiddleware.cs:380-388 ReadResult rr = await reader.ReadAtLeastAsync(len, ctx.RequestAborted); ReadAtLeastAsync returns with IsCompleted == true && Buffer.Length < len if the writer completes early. ReadOnlySequence.Slice(0, len) then throws ArgumentOutOfRangeException, which isn't in the inner catch list (InvalidDataException or IndexOutOfRangeException or EndOfStreamException) and falls through to 500. Spec (ssz-encoding.md:148) says malformed SSZ → 400. Fix: check rr.Buffer.Length < len after the read and throw EndOfStreamException (already mapped to 400), or add ArgumentOutOfRangeException to the inner catch.
File: SszCodecTests.cs Capabilities_roundtrip, DecodeGetBlobsRequest_roundtrip, DecodeGetPayloadBodiesByHashRequest_roundtrip, DecodeClientVersionRequest_roundtrip, etc. encode and decode through the same generator, so they pass regardless of wire shape. That's how finding 1. went undetected. The earlier rounds added byte-offset assertions for ExecutionPayload, PayloadStatusV1, ForkchoiceUpdatedResponseV1, PayloadAttributesV3 — the rest still need them. Fix: for each container type, add one explicit byte-layout test. For capabilities specifically, encode ["abc"] and assert exact length (4 outer + 4 inner offset + 3 = 11 once 1. is fixed) and the offset bytes themselves. Same pattern for blobs requests, payload bodies, client version, get-blobs response. 1., 2., 3. are the spec-conformance fixes. 4. is the small status-code correctness bug. 5. is the testing-pattern change that prevents future 1s. |
|
Claude finished @svlachakis's task in 3m 54s —— View job Analyzing @svlachakis's findings Tasks
|
| } | ||
|
|
||
| [SszContainer] | ||
| public partial struct SszCapabilityName |
There was a problem hiding this comment.
High — SszCapabilityName wire format diverges from spec (missing isCollectionItself: true)
The spec (execution-apis#764 ExchangeCapabilitiesResponse) defines each capability as ByteList[64] — raw bytes with no container header. SszTransaction uses the same pattern and is correctly annotated with [SszContainer(isCollectionItself: true)].
Without isCollectionItself: true, the generator emits a container codec: each SszCapabilityName in the Capabilities[] array is encoded as [4-byte offset → Name][Name bytes] (7 bytes for "abc"). With the flag set, it encodes as just [Name bytes] (3 bytes for "abc").
The resulting outer buffer for ["abc"]:
- Current (wrong): outer-offset(4) + array-offset(4) + inner-container-offset(4) + bytes(3) = 15 bytes
- Spec-correct: outer-offset(4) + array-offset(4) + bytes(3) = 11 bytes
The Capabilities_roundtrip test passes regardless because encode and decode share the same (wrong) layout — this is exactly the roundtrip-blindness that finding #5 calls out.
Fix:
[SszContainer(isCollectionItself: true)]
public partial struct SszCapabilityName
{
[SszList(64)] public byte[]? Name { get; set; }
}Apply the same fix to ExchangeCapabilitiesRequestWire — same field, same problem on the decode side when a CL sends its capabilities list.
A spec-vector test:
byte[] encoded = Encode<IReadOnlyList<string>>(["abc"], SszCodec.EncodeCapabilitiesResponse);
encoded.Length.Should().Be(11); // 4 outer + 4 array-offset + 3 bytes| public const byte Invalid = 1; | ||
| public const byte Syncing = 2; | ||
| public const byte Accepted = 3; | ||
| public const byte InvalidBlockHash = 4; |
There was a problem hiding this comment.
High — InvalidBlockHash = 4 is not defined in the SSZ spec; V1 newPayload can return it
The SSZ-REST spec (execution-apis#764, PayloadStatusV1 definition) defines exactly four values: VALID=0, INVALID=1, SYNCING=2, ACCEPTED=3. INVALID_BLOCK_HASH was explicitly dropped from the SSZ wire enumeration per the spec author.
The Paris engine API path (engine_newPayloadV1) can still return PayloadStatus.InvalidBlockHash from the domain layer when block-hash validation fails. EngineStatusToSsz maps this to 4, which no spec-compliant CL knows how to interpret — it will either treat it as an unexpected byte or a parse error.
Fix — normalize InvalidBlockHash → Invalid in the SSZ encode path:
PayloadStatus.InvalidBlockHash => SszPayloadStatus.Invalid,And remove SszPayloadStatus.InvalidBlockHash = 4 entirely, since it should not appear on the wire.
Note: the INVALID_BLOCK_HASH constant itself in PayloadStatus.cs and its use in JSON-RPC should stay unchanged; this normalization is only in the SSZ codec path.
| ReadResult rr = await reader.ReadAtLeastAsync(len, ctx.RequestAborted); | ||
| // Slice to the declared ContentLength even if Kestrel buffered extra bytes | ||
| // (HTTP keep-alive could carry the next request's framing in the same buffer). | ||
| return rr.Buffer.Slice(0, len); |
There was a problem hiding this comment.
Medium — truncated body throws ArgumentOutOfRangeException → 500 instead of 400
ReadAtLeastAsync(len) returns with IsCompleted = true and Buffer.Length < len when the writer closes before sending len bytes (truncated body). rr.Buffer.Slice(0, len) then throws ArgumentOutOfRangeException because len > Buffer.Length.
ArgumentOutOfRangeException is not in the inner catch list (InvalidDataException or IndexOutOfRangeException or EndOfStreamException), so it falls through to the outer catch (Exception ex) → 500 "Internal server error". The spec requires 400 for malformed/truncated bodies.
Fix — check length before slicing:
ReadResult rr = await reader.ReadAtLeastAsync(len, ctx.RequestAborted);
if (rr.Buffer.Length < len)
throw new EndOfStreamException($"Expected {len} bytes but stream ended with {rr.Buffer.Length}");
return rr.Buffer.Slice(0, len);EndOfStreamException is already in the 400-mapped catch list.
| @@ -0,0 +1,98 @@ | |||
| // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited | |||
There was a problem hiding this comment.
Medium — TransitionConfigurationSszHandler and its wire types should be removed; the SSZ-REST spec deleted this endpoint
The spec (execution-apis#764) endpoint summary does not include POST /engine/v1/transition-configuration. The spec author removed it (confirmed deleted in commit aca7eb6 of that PR). Currently, the implementation:
- Serves the SSZ-REST path
POST /engine/v1/transition-configuration - Advertises it in
engine_exchangeCapabilities(gated onpreCancun = !spec.IsEip4844Enabled) - Carries
TransitionConfigurationSszHandler.cs,TransitionConfigurationV1Wire,ExchangeTransitionConfigurationWire, and theDecodeTransitionConfigurationRequest/EncodeTransitionConfigurationResponsecodec methods
Since all production networks are post-Cancun, the capability is never advertised and the handler is never reachable in practice. But a pre-Cancun devnet (e.g., testing with an old chain) would advertise this non-spec endpoint, and a spec-compliant CL would reject it or ignore it.
Fix: delete the handler, the SSZ wire types (TransitionConfigurationV1Wire, ExchangeTransitionConfigurationWire), the codec methods, the SszRestPaths.TransitionConfiguration / PostV1TransitionConfig constants, and the EngineRpcCapabilitiesProvider entry. The JSON-RPC engine_exchangeTransitionConfigurationV1 is independent and should stay.

Replaces #10728
Summary
Implements the binary SSZ-REST Engine API transport per ethereum/execution-apis#764. The binary transport coexists with the existing JSON-RPC endpoint on the same authenticated port (8551) and is negotiated via
engine_exchangeCapabilities— no breaking changes to existing behaviour.Architecture
Wire types (
SszWireTypes.cs) —[SszContainer]definitions for every Engine-API structure across Paris → Amsterdam: payload V1–V4, forkchoice, blob bundles V1/V2,BlobAndProofV1/V2, payload bodies V1/V2, capabilities, client version, transition configuration. Driven by the existingNethermind.Serialization.SszGeneratorsource generator.Codec (
SszCodec.cs) — Stateless bridge between domain objects (ExecutionPayload,ForkchoiceStateV1, …) and wire types. Encoders write straight into anIBufferWriter<byte>, returning a byte count instead of a pooled buffer. Decoders acceptReadOnlySequence<byte>. Status-enum mapping, nullable-list (List[T, 1]) handling, little-endianUInt256layout forBaseFeePerGas/TerminalTotalDifficulty.Middleware (
SszMiddleware.cs) — ASP.NET Core middleware on the authenticated Engine-API port. Triggers onContent-Type: application/octet-stream(POST) orAccept: application/octet-stream(GET). Performs JWT auth, route resolution (/engine/v{N}/{resource}[/{extra}]), 16 MiB body-size enforcement, and dispatches to the appropriate versioned handler. Non-SSZ requests pass through to JSON-RPC unchanged. Error-code → HTTP-status mapping (404,409,413,422,500) is per-spec.Handler dispatch — Generic versioned handlers parameterized by per-version descriptor structs (
INewPayloadVersion<TWire>,IForkchoiceUpdatedVersion<TWire>,IGetPayloadVersion<TResult>,IPayloadBodiesByHash/RangeVersion<TResult>,IGetBlobsV2Version). Adding a new endpoint version is one descriptor struct + one DI registration — no version switches anywhere. Every descriptor calls intoIEngineRpcModule.engine_*V{N}, so JSON-RPC and SSZ-REST share the same engine-module entry points.Capabilities provider (
EngineRpcCapabilitiesProvider.cs) — SingleBuildmethod emits bothengine_*V{N}JSON-RPC method names and SSZ-REST path strings, each gated by the same fork checks via a shared helper. Capability options live in[Flags] enum RpcCapabilityOptions : byte { None, Enabled, WarnIfMissing }withIsEnabled()/ShouldWarnIfMissing()extensions.DI bridge (
SszMiddlewareConfigurer.cs) —IJsonRpcServiceConfigurerthat bridges Autofac (where Engine-API handlers live) to ASP.NET's MS DI container so Kestrel can resolveSszMiddlewareand its handlers without duplicating registration.Performance
Encode side — Encoders write directly into
ctx.Response.BodyWriter(aPipeWriter) viaGetSpan/Encode/Advance/ setContentLengthfrom the returned byte count /FlushAsync. No intermediateArrayPoolSpan<byte>rent + copy, noMemoryStreamrebuild. Connection is aborted if encode throws mid-stream so the CL never sees a500with garbled binary.Decode side — Middleware reads via
PipeReader.ReadAtLeastAsync(ContentLength)and hands the resultingReadOnlySequence<byte>straight to the generator-emittedDecode(ReadOnlySequence<byte>, out T)overload. Single-segment input is zero-copy; multi-segment consolidates once viaArrayPoolthen dispatches to the existing span decoder. Eliminates theMemoryStream + ToArrayintermediate copy that was dominant on blob-bearing payloads.SszLibgains primitive sequence overloads (bool/byte/ushort/uint/int/ulong/long/UInt128/UInt256/BitArray) as infrastructure for a future fully-recursive sequence-aware decoder.Routing —
FrozenDictionarywithAlternateLookup<ReadOnlySpan<char>>for zero-alloc resource lookup. Pathextra(e.g.payloadId) threaded asReadOnlyMemory<char>end-to-end.Span<char>-based hex parse for payload IDs avoids substring allocation.Wire-encoder dedup —
MerkleizeRefType{Vector,List,ProgressiveList}collapsed into one core helper; small chunks stack-allocate (≤4 chunks), larger payloads useArrayPool. Single-allocation Blob V1/V2 encoders (count nulls first, allocate exact size).Spec conformance
Aligned with ethereum/execution-apis#764:
application/octet-streamcontent/accept negotiation400 Bad Requestfor malformed SSZ bodies404 Not Foundfor unknown payload IDs / endpoints409 ConflictforInvalidForkchoiceState413 Payload Too Largepast the 16 MiB limit422 Unprocessable EntityforInvalidPayloadAttributes500 Internal Server ErrorforInternalErrorFollow-up
Issue #11525 tracks SSZ generator support for
InlineArray<T, N>to remove thebyte[]allocation inBytes32/Hash256value paths.What types of changes does your code introduce?
Testing
Requires testing
If yes, did you write tests?
Notes on testing
SszCodecTests(29 tests) — Encode/decode roundtrips for payload status, forkchoice response, transition configuration, capabilities, blob requests, payload bodies V1/V2, andgetPayloadresponses V1–V6. Spec-defined byte-offset assertions for fixed sections ofExecutionPayload,PayloadStatus,ForkchoiceUpdatedResponse,GetPayloadResponseV3,GetPayloadResponseV4,PayloadAttributesV3(regression for theparent_beacon_block_rootplacement bug).SszMiddlewareTests(26 tests) — DriveSszMiddlewarethroughDefaultHttpContext(no Kestrel required). Verify handler dispatch per version, route fallback (versioned → version-agnostic),Cache-Control: no-storeongetPayloadresponses, status-code mapping, capability advertising, and 401/404/413/422 error paths.SszMultiSegmentDecodeTests(26 tests) — Cover the multi-segmentReadOnlySequence<byte>branch that production traffic from Kestrel actually hits (4 KB pooled blocks). Builds explicit multi-segment sequences viaReadOnlySequenceSegment<byte>chains at boundary sizes 1/3/7/4096 across five wire types (TransitionConfig, Capabilities, GetBlobsRequest, GetPayloadBodiesByRange, NewPayloadV3RequestWire) and theSszLibprimitive boundary path (uint/ulong/UInt256).