Context
Discussed in #11301. The SSZ source generator currently emits wrapper containers like SszKzgCommitment and SszBlob as plain structs containing a byte[] field:
[SszContainer]
public partial struct SszKzgCommitment
{
[SszVector(48)] public byte[]? Bytes { get; set; }
}
[SszContainer]
public partial struct SszBlob
{
[SszVector(0x2_0000)] public byte[]? Bytes { get; set; } // 131,072 bytes
}
Per blob bundle that's up to 4096 SszKzgCommitment instances (V1) or more (V2), each carrying its own byte[48] heap allocation. The outer array adds another. So encoding one V2 blobs-bundle costs 4096 × 2 + outer arrays ≈ 8000+ small allocations for the proofs alone.
For the small fixed-size cases (32-byte Hash256, 48-byte commitments/proofs), C# 12's [InlineArray(N)] would let the generator emit an inline-stored struct:
[InlineArray(48)]
public struct SszKzgCommitment
{
private byte _e0;
}
Then SszKzgCommitment[] is a single contiguous heap allocation of count * 48 bytes (no per-element header) and the wire format is identical (SSZ for a fixed-size byte vector is just the raw bytes).
What's in scope
Generator-side support for [InlineArray(N)] byte-vector backing on small fixed-size byte-vector containers:
SszKzgCommitment (48) — commitments, proofs in BlobsBundleV{1,2}
- Hash arrays in
SszCodec.HashesFromWire (32 each) — currently allocates byte[32] per hash
- Any future fixed-size 32/48/64/96-byte byte-vector container
What's NOT in scope
SszBlob (131,072 bytes) — too large to InlineArray. A 4096-blob bundle would be ~512 MiB inline; stack/struct copies become catastrophic. Keeps byte[] backing.
- Variable-length lists (
[SszList]) — InlineArray is fixed-size by definition.
- Bitvectors / bitlists — different shape.
Threshold
Suggest a generator-side threshold (e.g., a [SszInline] opt-in attribute, or auto-promote when [SszVector(N)] and N <= 96). The threshold should be conservative — small fixed sizes only.
Why now
#11301 introduces SSZ-REST transport with raw blob payloads on the hot CL→EL path. Per-request allocations in BlobsBundleV{1,2}Wire encoding are a measurable contributor at high blob counts (post-Osaka with MAX_BLOB_COMMITMENTS_PER_BLOCK = 4096).
Acceptance criteria
- Generator detects opt-in (or small-fixed-size auto) and emits
[InlineArray(N)] struct.
- Encode/decode/merkleize paths work for inline-array-backed types — bytes are accessed as
MemoryMarshal.CreateSpan.
- BlobsBundle encoding path measurably reduces allocations on a benchmark (e.g.,
4096-commitment BlobsBundle encode).
- Existing
SszBlob (131,072) keeps byte[] — explicit non-conversion.
- All existing SSZ generator tests pass.
Notes
- C# 12+ /
net8.0+ required for [InlineArray]. Repo targets net10.0 ✓
- The user-facing wrapper types (
SszKzgCommitment, etc.) are internal to Nethermind.Merge.Plugin.SszRest and the generator — no external consumers.
Context
Discussed in #11301. The SSZ source generator currently emits wrapper containers like
SszKzgCommitmentandSszBlobas plain structs containing abyte[]field:Per blob bundle that's up to 4096
SszKzgCommitmentinstances (V1) or more (V2), each carrying its ownbyte[48]heap allocation. The outer array adds another. So encoding one V2 blobs-bundle costs4096 × 2 + outer arrays ≈ 8000+ small allocationsfor the proofs alone.For the small fixed-size cases (32-byte
Hash256, 48-byte commitments/proofs), C# 12's[InlineArray(N)]would let the generator emit an inline-stored struct:Then
SszKzgCommitment[]is a single contiguous heap allocation ofcount * 48bytes (no per-element header) and the wire format is identical (SSZ for a fixed-size byte vector is just the raw bytes).What's in scope
Generator-side support for
[InlineArray(N)]byte-vector backing on small fixed-size byte-vector containers:SszKzgCommitment(48) — commitments, proofs inBlobsBundleV{1,2}SszCodec.HashesFromWire(32 each) — currently allocatesbyte[32]per hashWhat's NOT in scope
SszBlob(131,072 bytes) — too large to InlineArray. A 4096-blob bundle would be ~512 MiB inline; stack/struct copies become catastrophic. Keepsbyte[]backing.[SszList]) — InlineArray is fixed-size by definition.Threshold
Suggest a generator-side threshold (e.g., a
[SszInline]opt-in attribute, or auto-promote when[SszVector(N)]andN <= 96). The threshold should be conservative — small fixed sizes only.Why now
#11301 introduces SSZ-REST transport with raw blob payloads on the hot CL→EL path. Per-request allocations in
BlobsBundleV{1,2}Wireencoding are a measurable contributor at high blob counts (post-Osaka withMAX_BLOB_COMMITMENTS_PER_BLOCK = 4096).Acceptance criteria
[InlineArray(N)]struct.MemoryMarshal.CreateSpan.4096-commitmentBlobsBundle encode).SszBlob(131,072) keepsbyte[]— explicit non-conversion.Notes
net8.0+ required for[InlineArray]. Repo targetsnet10.0✓SszKzgCommitment, etc.) are internal toNethermind.Merge.Plugin.SszRestand the generator — no external consumers.