feat(merge): add SSZ-REST transport for Engine-API#11301
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 |
…engine, split codec/round-trip The previous benchmark compared SSZ-through-real-middleware against a hand-rolled JSON stub that bypassed JsonRpcProcessor and did its own ad-hoc parse-twice (JsonDocument + per-payload Deserialize) with a static byte[] response. The two paths weren't measuring equivalent work, which made the SSZ-vs-JSON comparison misleading. JSON path now goes through the production RpcModuleProvider + JsonRpcService + JsonRpcProcessor with the engine module registered as a SingletonModulePool, so both paths share the same engine call surface and only the transport + codec differ. Other changes: - Replace 130-line StubEngineModule (mostly NotImplementedException placeholders) with NSubstitute on IEngineRpcModule. - Reuse TestItem.KeccakA-E and TestItem.AddressA-B; drop local consts. - Cache MediaTypeHeaderValue and the parsed Authorization as static fields; return int (status code) from round-trip benchmarks instead of HttpResponseMessage. - Tighten EncodeSszBody (raw byte[], drops ArrayBufferWriter detour) and EncodeJsonBody (Utf8JsonWriter + JsonSerializer.Serialize directly into the writer; no JsonDocument injector pattern). - Single BuildEngineHost helper; SSZ and JSON servers diverge only in service registration and the terminal middleware. - Drop StubProcessExitSource from the JSON server (only SszMiddleware needs it). - Split into 6 benchmarks: deserialize Ssz/Json, serialize PayloadStatus Ssz/Json, full round-trip Ssz/Json. [Params(0,1,3,6)] for blob count. - Add NSubstitute and Nethermind.Core.Test references to the benchmark csproj. Validated: 24/24 benchmarks pass. Headline (Blobs=6): SSZ decode 2.4us vs JSON 7.2us (3x); SSZ encode 27ns vs JSON 110ns (4x); round-trip SSZ 78us vs JSON 121us (1.5x). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nchmarks; simplify builders; expand blob params
The class covers more than just an end-to-end round-trip — it also runs
codec-only deserialize and PayloadStatus serialize benchmarks. Rename the file
and class to reflect that. Update the runner's Program.cs reference accordingly.
BuildTransactions: drop the per-tx Random fill — content doesn't affect codec
work or output size, so the random bytes were noise. Just allocate and set the
EIP-1559 type byte.
BuildBlobVersionedHashes: lift the byte buffer out of the loop and use stackalloc
+ Hash256(ReadOnlySpan<byte>) so we don't allocate one byte[32] per blob. With
[Params] now extending up to 72 blobs, the per-iteration allocation is worth
removing.
[Params] expanded from {0, 1, 3, 6} to {0, 1, 3, 6, 12, 24, 36, 72} to surface
how cost scales beyond the spec maximum.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BDN's default 120s build timeout for the auto-generated boilerplate trips when the benchmark project's transitive dep graph is large (Merge.Plugin pulls in JsonRpc, Synchronization, Init, etc., and a cold restore + build of all of those takes longer than 2 minutes on a regular machine). Set ManualConfig.BuildTimeout to 10 minutes — covers a clean build, no effect once the dep tree is warm. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rawals The previous defaults (50 × 300 B = 15 KB tx data, 2 withdrawals) were nowhere near a realistic mainnet block. The codec cost the benchmark measured was mostly fixed-field overhead, not the variable payload weight that dominates in production. Bump the tx-data baseline to 250 × 600 B ≈ 150 KB — matches a typical mainnet block. The constant is a single edit-point if someone wants to stress-test worst case (e.g. 2500 × 600 B ≈ 1.5 MB observed on mainnet). Withdrawals go from 2 to 16 (Capella max). BuildWithdrawals fills them from TestItem.Addresses to keep the payload realistic. Note: blob count is the right scaling axis for V3 specifically — the envelope carries 32-byte versioned hashes only, not blob bytes (which travel in a sidecar). So 72 blobs adds 2.3 KB to the envelope, not 9 MB. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Host.CreateDefaultBuilder() wires the Microsoft.AspNetCore.Hosting.Diagnostics
logger at Information level by default, which floods stdout with two lines
("Request starting" / "Request finished") per benchmark iteration. Drop all
log providers — BenchmarkDotNet has its own output pipeline and any other
log noise is benchmark-irrelevant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…; reuse Eip4844Constants Adds a sibling benchmark targeting engine_getPayloadV3, where the SSZ-vs-JSON delta is most pronounced — every proposing slot returns ExecutionPayloadV3 + BlobsBundle (commitments, proofs, ~131 KB blobs). JSON hex-encodes blob bytes, doubling the wire size; SSZ keeps them as-is. Layout matches NewPayloadSerializationBenchmarks: SSZ decode wire, JSON parse JsonDocument (no STJ-constructible target — production CL has its own DOM), SSZ/JSON encode of the result, full Kestrel round-trip via SszMiddleware / real JsonRpcProcessor with engine_getPayloadV3 stubbed via NSubstitute. Block construction uses Build.A.Block with real signed EIP-1559 transactions (Build.A.Transaction...Signed). Going through ExecutionPayloadV3.TryGetBlock() on a payload with synthetic byte[] transactions failed RLP decode, so the benchmark constructs the Block directly. Reuse existing constants where they exist: - Eip4844Constants.GasPerBlob (= 131072) for blob byte size and BlobGasUsed. - Eip4844Constants.BytesPerBlobVersionedHash (= 32) for hash buffer size. - KzgPolynomialCommitments.KzgBlobHashVersionV1 (= 0x01) for the version byte. 48 (commitment / proof bytes) stays a local const — the canonical source is Ckzg.BytesPerCommitment / BytesPerProof from CkzgLib, which would require a new namespace reference for two named constants. Validated: 48/48 benchmarks pass. At Blobs=72 (worst case): SSZ round-trip 4.1ms / 9.6 MB vs JSON 43.3ms / 115 MB — 10x faster, 12x fewer allocations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Benchmark: |
|
|
@claude confirm or refute these issues; comment on the code with fixes if found to be true
The SSZ middleware checks url.IsAuthenticated but not url.RpcEndpoint.HasFlag(RpcEndpoint.Http). JSON-RPC startup has this HTTP endpoint guard, so SSZ REST can be served on authenticated ports configured as WebSocket-only. Require the HTTP flag, and ideally the Engine module flag, before handling SSZ REST requests.
|
|
Claude finished @benaadams's task in 3m 29s —— View job Analyzing @benaadams's findings — all five verified against the current HEAD. Tasks
Issue 1 — ✅ Confirmed: Empty withdrawals silently collapsed to null
set
{
field = value;
if (value is null || value.Length == 0) // ← collapses [] and null to the same thing
{
_inner.Withdrawals = null;
return;
}
...
}A V2+ Fix: distinguish set
{
field = value;
if (value is null)
{
_inner.Withdrawals = null;
return;
}
Withdrawal[] result = new Withdrawal[value.Length];
for (int i = 0; i < result.Length; i++)
result[i] = new Withdrawal { ... };
_inner.Withdrawals = result;
}Issue 2 — ✅ Confirmed: ResultWrapper carrying BlobsV1DirectResponse is never disposed
protected static Task WriteSszResultAsync<T>(HttpContext ctx, ResultWrapper<T> result, ...) =>
result switch
{
{ Result.ResultType: not ResultType.Success } => WriteErrorAsync(...),
{ Data: null } => SetNoContent(ctx),
{ Data: var data } => WriteSszAsync(ctx, data, encode)
// ← result is never disposed
};
Fix: add a protected static async Task WriteSszResultAsync<T>(HttpContext ctx, ResultWrapper<T> result, Func<T, IBufferWriter<byte>, int> encode)
{
using (result)
{
await (result switch
{
{ Result.ResultType: not ResultType.Success } => WriteErrorAsync(ctx, ErrorCodeToHttpStatus(result.ErrorCode), result.Result.Error ?? "Unknown error"),
{ Data: null } => SetNoContent(ctx),
{ Data: var data } => WriteSszAsync(ctx, data, encode)
});
}
}Issue 3 — ✅ Confirmed: Fixed-size ref-type list decode silently truncates non-aligned input
string countExpr = property.Kind == Kind.Vector
? property.Length!.Value.ToString()
: $"{sliceExpression}.Length / {itemSize}"; // ← integer division, no remainder checkFor any Fix: emit a remainder check before computing count: // Generated code should be:
if ({sliceExpression}.Length % {itemSize} != 0)
throw new InvalidDataException($"Expected multiple of {itemSize} bytes for {typeName}.{fieldName}");
int __count = {sliceExpression}.Length / {itemSize};In string guard = property.Kind == Kind.List
? $"if ({sliceExpression}.Length % {itemSize} != 0) throw new System.IO.InvalidDataException($\"{decl.Name}.{property.Name}: expected multiple of {itemSize} bytes, got {{{sliceExpression}.Length}}\");"
: string.Empty;
string loop = $"{{ {guard} int __count = {countExpr}; ...";Issue 4 — ✅ Confirmed: SSZ middleware ignores the HTTP endpoint flag
else if (!_urlCollection.TryGetValue(ctx.Connection.LocalPort, out JsonRpcUrl? url) || !url.IsAuthenticated)
{
await _next(ctx);
}The JSON-RPC pipeline at if (!jsonRpcUrlCollection.TryGetValue(ctx.Connection.LocalPort, out JsonRpcUrl jsonRpcUrl) ||
!jsonRpcUrl.RpcEndpoint.HasFlag(RpcEndpoint.Http)) // ← present in JSON-RPCThe SSZ middleware omits the Fix: else if (!_urlCollection.TryGetValue(ctx.Connection.LocalPort, out JsonRpcUrl? url)
|| !url.IsAuthenticated
|| !url.RpcEndpoint.HasFlag(RpcEndpoint.Http)) // ← add this
{
await _next(ctx);
}Issue 5 — ✅ Confirmed: Publication race —
|
| # | Issue | Severity | Verdict |
|---|---|---|---|
| 1 | Empty withdrawals → null: valid V2+ blocks rejected on SSZ path |
High | ✅ Confirmed |
| 2 | ResultWrapper<BlobsV1DirectResponse> never disposed: ArrayPoolList leaked |
Medium | ✅ Confirmed |
| 3 | Ref-basic-type SSZ list: non-aligned bytes silently truncated | Medium | ✅ Confirmed |
| 4 | SSZ middleware ignores RpcEndpoint.Http flag: serves WS-only ports |
Low | ✅ Confirmed |
| 5 | EnsureBuilt() race: _ssz readable as null after _jsonRpc published |
Medium | ✅ Confirmed |
All five are real bugs. Issue 1 is the most impactful — it is a consensus correctness regression that rejects valid post-Shanghai blocks with zero withdrawals on the SSZ path. Issues 2, 3, and 5 are resource-management / thread-safety defects. Issue 4 is a configuration-boundary inconsistency.
|
@claude also consider and confirm or refute in same way
|
|
Claude finished @benaadams's task in 5m 5s —— View job Analyzing @benaadams's findings — all three verified against current HEAD. Tasks
Finding 1 — ✅ Confirmed:
|
| # | Finding | Severity | Verdict |
|---|---|---|---|
| 1 | getPayloadBodiesByRange count > 32 triggers full engine work before 422 rejection |
Medium | ✅ Confirmed |
| 2 | BlockNumber/GasLimit/GasUsed unchecked ulong→long cast wraps to negative |
High | ✅ Confirmed |
| 3 | Variable-length SSZ list elements allocated before ValidateSszListLimit fires |
Medium | ✅ Confirmed |
Finding 2 is the most impactful — an authenticated CL can set consensus-critical fields to negative values via crafted SSZ, potentially corrupting fork-spec lookups and gas accounting on the Nethermind side. Findings 1 and 3 are bounded-cost denial-of-service vectors that waste engine work or trigger over-allocation before rejection.
|
@claude re-review |
|
Claude finished @benaadams's task in 5m 32s —— View job Re-review — all 8 findings confirmed fixed ✅Verified each finding from both of @benaadams' comment rounds against the current branch. Round 1 — all 5 fixed
Round 2 — all 3 fixed
One minor observation (non-blocking)In All findings addressed. Ready to merge from a correctness standpoint. |
NewPayloadSerializationBenchmarks and GetPayloadV3SerializationBenchmarks were duplicating ~150 lines of identical plumbing: host builder, JSON-RPC processor wiring, stub URL collection / auth / process-exit, header constants, and the withdrawal factory. Extract to EngineBenchmarkHost (internal static): Build(...) for the engine TestServer scaffolding, BuildJsonServer(engine) for the JSON-RPC pipeline, BuildWithdrawals(count), shared header values, and the three Stub* classes. Both benchmark files now keep only their format-specific encoding / decoding helpers and the SSZ handler registration. No behavior change. Build clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eGetPayloadBodiesByRangeRequest Address benaadams's review on SszExecutionPayload.cs:70 — the 5 inline "if (value > (ulong)long.MaxValue) throw …" property setters share an identical shape but no shared code. Extract to a single SszNumericChecks.CheckedLong helper with the suggested cold-path local function + [DoesNotReturn, StackTraceHidden] for clean JIT codegen. While there, fix the unchecked ulong→long cast in SszCodec.DecodeGetPayloadBodiesByRangeRequest: wire.Start and wire.Count arrived as long via direct (long)wire.Count, so values in (long.MaxValue, ulong.MaxValue] wrapped to negative. The handler-side "count > 32" guard then passed (negative isn't > 32), and the engine's "count < 1" guard caught it with a generic 400 "must be positive". No security impact, but the SSZ-spec limit message was being skipped. With CheckedLong, the same out-of-range condition now produces a proper SSZ 400 "uint64 value … exceeds valid range for long" at decode time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nerator The attribute existed so consumers could declare new SSZ basic types out-of-tree, but the only two users (SszBytes8, SszKzgCommitment) lived inside Nethermind itself. Per-compilation discovery (DiscoverKnownTypes walking the namespace tree, attribute symbol lookup, WeakReference cache) was ~80 lines of generator code in service of two types. Move SszBytes8 and SszKzgCommitment to Nethermind.Serialization.Ssz so every project that depends on the SSZ generator can see them, then hardcode them in SszType.BasicTypes alongside SszBytes32. Generator no longer needs the discovery pass; the [SszBasicType] attribute and its attribute class are deleted. Adding a new fixed-length basic SSZ type now means one entry in SszType.BasicTypes — same friction as the existing Hash256 / Address / Bloom entries. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After SszBytes8 and SszKzgCommitment moved to Nethermind.Serialization.Ssz, the remaining wire types in this file don't use System, InteropServices, or CompilerServices types — only the SSZ generator attributes plus core/crypto. Drops three dead usings; resolves IDE0005 warning surfaced by CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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).