Skip to content

StructureSizeTest: Phase0/Electra attestation fixtures use byte counts where Bitlist requires bit counts #625

Description

@shane-moore

max_agg_comm_consensus_data.go constructs aggregation_bits for the max Phase0/Electra attestation fixtures by allocating MAX_VALIDATORS_PER_* bytes, where the SSZ spec defines the field as a Bitlist whose size parameter is in bits:

Per consensus-specs, aggregation_bits is Bitlist[MAX_VALIDATORS_PER_COMMITTEE] (Phase0) and Bitlist[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT] (Electra), with MAX_VALIDATORS_PER_COMMITTEE = 2048, MAX_COMMITTEES_PER_SLOT = 64. The SSZ Bitlist encoding packs N data bits plus a single delimiter bit into ⌈(N+1)/8⌉ bytes:

Bitlist Max canonical encoded bytes
Bitlist[2048] 257
Bitlist[131072] 16385

So the spec-correct max attestation sizes are:

Type Spec-correct max Constant in ssv-spec Inflation
phase0.Attestation 4 + 128 + 96 + 257 = 485 MaxSizePhase0Attestation = 2276 ~4.7x
electra.Attestation 4 + 128 + 96 + 8 + 16385 = 16621 MaxSizeElectraAttestation = 131308 ~7.9x

The bytes produced are also non-canonical: [0x01, 0x00, …, 0x00] puts the delimiter at bit 0 (length = 0 bits) followed by 2047/131071 trailing zero bytes. Strict SSZ decoders reject this — including fastssz's own ValidateBitlist, so even go-eth2-client's generated Attestation.UnmarshalSSZ (which ssv-spec uses) would fail to round-trip these fixtures. The current test only calls Object.Encode(), so it never hits the decoder check.

Two related causes let this through:

  1. The fastssz-generated encoder for bitfield.Bitlist checks len(bytes) <= 2048, treating the ssz-max:"2048" tag as a byte limit. The SSZ-spec interpretation is bits — the encoder check is the same dimension confusion.
  2. checkSSZTags compares Len() (byte count for bitfield.Bitlist / []byte) against the ssz-max tag — same confusion in the validator.

Also affects MaxSizeAggregatorCommitteeConsensusData = 8970524, since AggregatedAttestations is budgeted as 64 × MaxSizeElectraAttestation. With the corrected per-attestation max, the aggregated-attestations region shrinks by ~8x.

Suggested fix (Phase0; analogous for Electra)

var aggbits [257]byte
for i := range aggbits[:256] { aggbits[i] = 0xFF }
aggbits[256] = 0x01 // delimiter at bit 2048

Blast radius

grep finds no production callers of these three constants — they're only used inside this fixture file. So no on-wire impact. But downstream consumers (other client impls, ops/budgeting docs) reading these as the canonical max-message sizes would over-allocate by ~8x, and any client that tries to round-trip these fixtures through a strict decoder will fail (which is how I found this — Anchor's strict SSZ decoder rejects the AggregationBits payload with InvalidByteCount { given: 2048, expected: 1 }).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions