Skip to content

SyncCommitteeContribution.AggregationBits: dynssz-size tag hardcodes SYNC_COMMITTEE_SUBNET_COUNT as literal 4 #300

Description

@AntiD2ta

Summary

The dynssz-size struct tag on SyncCommitteeContribution.AggregationBits hardcodes the value of SYNC_COMMITTEE_SUBNET_COUNT as the literal 4 instead of referencing the spec constant by name. This makes the dynamic-SSZ size of the field independent of the actual runtime spec, so under any preset where SYNC_COMMITTEE_SUBNET_COUNT != 4 the field is sized incorrectly.

Location

spec/altair/synccommitteecontribution.go:36

// AggregationBits size is SYNC_COMMITTEE_SIZE // SYNC_COMMITTEE_SUBNET_COUNT
AggregationBits bitfield.Bitvector128 `dynssz-size:"SYNC_COMMITTEE_SIZE/4/8" ssz-size:"16"`

The comment on the line directly above states the intended size is SYNC_COMMITTEE_SIZE // SYNC_COMMITTEE_SUBNET_COUNT, but the tag substitutes the literal 4 for SYNC_COMMITTEE_SUBNET_COUNT.

Why this looks wrong

Every other dynssz-size expression in the tree derives its size from named spec constants — this is the one place a magic number appears in the divisor:

File Field dynssz-size
spec/electra/attestation.go CommitteeBits MAX_COMMITTEES_PER_SLOT/8
spec/altair/syncaggregate.go SyncCommitteeBits SYNC_COMMITTEE_SIZE/8
spec/altair/synccommitteecontribution.go AggregationBits SYNC_COMMITTEE_SIZE/**4**/8

The whole point of dynssz-size (versus the static ssz-size) is to let the encoder recompute field sizes from the runtime config when a non-standard preset is loaded (http.WithCustomSpecSupport(true)). Baking in 4 defeats that for this field: the dynamic path will keep computing SYNC_COMMITTEE_SIZE / 4 / 8 regardless of what SYNC_COMMITTEE_SUBNET_COUNT actually is in the loaded spec.

Impact

SyncCommitteeContribution.AggregationBits is a bitvector of SYNC_COMMITTEE_SIZE / SYNC_COMMITTEE_SUBNET_COUNT bits. Under a custom preset that changes SYNC_COMMITTEE_SUBNET_COUNT away from 4, the dynamic-SSZ size for this field is wrong, which corrupts marshal/unmarshal and HashTreeRoot for SyncCommitteeContribution (and anything embedding it) on that preset.

This is latent for the standard networks: both the mainnet and minimal presets define SYNC_COMMITTEE_SUBNET_COUNT = 4, so SYNC_COMMITTEE_SIZE / 4 / 8 happens to be correct there, and the consensus-spec test vectors (which run against those two presets) pass. The bug only surfaces on a custom preset with a different subnet count.

Suggested fix

Use the spec constant name in the divisor, matching the sibling fields and the accompanying comment:

AggregationBits bitfield.Bitvector128 `dynssz-size:"SYNC_COMMITTEE_SIZE/SYNC_COMMITTEE_SUBNET_COUNT/8" ssz-size:"16"`

If the tag is generated rather than hand-written, the generator template / source annotation for this field should be updated so the named constant is emitted. If there's a reason the multi-variable expression can't be used here (e.g. a codegen limitation), it would help to capture that in a comment so the literal 4 doesn't read as an oversight.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions