Goal
ssv_types::ValidatorIndex SSZ-encodes to exactly 8 little-endian bytes on every compilation target, matching the uint64 that the Ethereum consensus spec, ssv-spec, and go-ssv define for validator indices.
Context / motivation
ValidatorIndex is pub struct ValidatorIndex(pub usize) with #[ssz(struct_behaviour = "transparent")] (anchor/common/ssv_types/src/cluster.rs:77-78), so SSZ encoding forwards to usize. In ethereum_ssz 0.10.1 (the version pinned by Cargo.lock), the usize Encode/Decode impls are gated on #[cfg(target_pointer_width)] (src/encode/impls.rs:40-44, src/decode/impls.rs:48-52): 8 bytes on 64-bit targets, 4 bytes on 32-bit targets, and decode rejects any other width.
Three protocol-visible containers embed the field, all of which are SSZ-encoded into SSVMessage.data, RSA-signed, and gossiped:
PartialSignatureMessage (anchor/common/ssv_types/src/partial_sig.rs:129-133)
ValidatorDuty (anchor/common/ssv_types/src/consensus.rs:447-451)
AssignedAggregator (anchor/common/ssv_types/src/consensus.rs:495-497)
A 32-bit build would therefore emit 4-byte index fields (a one-entry PartialSignatureMessages payload shrinks from 164 to 160 bytes) and fail to decode the 8-byte fields produced by go-ssv, which uses phase0.ValidatorIndex (uint64) in the same message.
This is a latent portability defect, not a live incompatibility: all release binaries (x86_64/aarch64 Linux, aarch64 Darwin) are 64-bit and byte-compatible with go-ssv today. Exposure is limited to source builds on 32-bit targets. Tree-hash roots are already width-independent (tree_hash 0.12.1 widens usize to u64), so only the SSZ serialization is affected. A wire type's byte layout should not depend on the pointer width of the machine that compiled it.
Suggested approach
Change the inner type to u64, matching the OperatorId(pub u64) precedent in the same crate. This is byte-identical on all 64-bit targets and makes the encoding width-independent by construction.
The change is net-simplifying because Lighthouse already hands Anchor u64 indices; the current narrowing casts at that boundary get deleted rather than guarded:
anchor/eth/src/index_sync.rs:140
anchor/validator_store/src/metadata_service.rs:334, :363, :728, :774
anchor/validator_store/src/lib.rs:2343
FromSql/ToSql (cluster.rs:80-94) keep their checked i64 conversions, so the on-disk representation is unchanged and no migration is needed. ssv_types has no serde surface, so no JSON/API impact.
An alternative is keeping pub usize and hand-writing fixed 8-byte Encode/Decode (as PartialSignatureKind does), but nothing in the codebase uses the inner usize as a collection index, so the extra code buys nothing and leaves a silent-truncation footgun on 32-bit constructors.
Acceptance criteria
ValidatorIndex SSZ-encodes to exactly 8 little-endian bytes independent of target pointer width, and its tree-hash root equals that of an SSZ uint64.
- Zero byte changes on 64-bit builds: a one-entry
PartialSignatureMessages payload remains 164 bytes.
- All conversions between the index and target-sized integers are checked; no silent truncation.
- Database rows containing
validator_index read back identically (i64 column unchanged, conversions still checked).
Tests
- Golden encoded-byte assertions for
ValidatorIndex(0), ValidatorIndex(1), and ValidatorIndex(u64::MAX).
- A one-entry
PartialSignatureMessages encode round-trip asserting the 164-byte layout.
- Width-independence follows by construction from delegating to
u64 (whose impls are not cfg-gated); a 32-bit cargo check in CI is optional reinforcement.
Notes
Issues are directionally correct, not prescriptive; verify symbols at PR time.
Goal
ssv_types::ValidatorIndexSSZ-encodes to exactly 8 little-endian bytes on every compilation target, matching theuint64that the Ethereum consensus spec, ssv-spec, and go-ssv define for validator indices.Context / motivation
ValidatorIndexispub struct ValidatorIndex(pub usize)with#[ssz(struct_behaviour = "transparent")](anchor/common/ssv_types/src/cluster.rs:77-78), so SSZ encoding forwards tousize. Inethereum_ssz 0.10.1(the version pinned byCargo.lock), theusizeEncode/Decodeimpls are gated on#[cfg(target_pointer_width)](src/encode/impls.rs:40-44,src/decode/impls.rs:48-52): 8 bytes on 64-bit targets, 4 bytes on 32-bit targets, and decode rejects any other width.Three protocol-visible containers embed the field, all of which are SSZ-encoded into
SSVMessage.data, RSA-signed, and gossiped:PartialSignatureMessage(anchor/common/ssv_types/src/partial_sig.rs:129-133)ValidatorDuty(anchor/common/ssv_types/src/consensus.rs:447-451)AssignedAggregator(anchor/common/ssv_types/src/consensus.rs:495-497)A 32-bit build would therefore emit 4-byte index fields (a one-entry
PartialSignatureMessagespayload shrinks from 164 to 160 bytes) and fail to decode the 8-byte fields produced by go-ssv, which usesphase0.ValidatorIndex(uint64) in the same message.This is a latent portability defect, not a live incompatibility: all release binaries (x86_64/aarch64 Linux, aarch64 Darwin) are 64-bit and byte-compatible with go-ssv today. Exposure is limited to source builds on 32-bit targets. Tree-hash roots are already width-independent (
tree_hash 0.12.1widensusizetou64), so only the SSZ serialization is affected. A wire type's byte layout should not depend on the pointer width of the machine that compiled it.Suggested approach
Change the inner type to
u64, matching theOperatorId(pub u64)precedent in the same crate. This is byte-identical on all 64-bit targets and makes the encoding width-independent by construction.The change is net-simplifying because Lighthouse already hands Anchor
u64indices; the current narrowing casts at that boundary get deleted rather than guarded:anchor/eth/src/index_sync.rs:140anchor/validator_store/src/metadata_service.rs:334,:363,:728,:774anchor/validator_store/src/lib.rs:2343FromSql/ToSql(cluster.rs:80-94) keep their checked i64 conversions, so the on-disk representation is unchanged and no migration is needed.ssv_typeshas no serde surface, so no JSON/API impact.An alternative is keeping
pub usizeand hand-writing fixed 8-byteEncode/Decode(asPartialSignatureKinddoes), but nothing in the codebase uses the innerusizeas a collection index, so the extra code buys nothing and leaves a silent-truncation footgun on 32-bit constructors.Acceptance criteria
ValidatorIndexSSZ-encodes to exactly 8 little-endian bytes independent of target pointer width, and its tree-hash root equals that of an SSZuint64.PartialSignatureMessagespayload remains 164 bytes.validator_indexread back identically (i64 column unchanged, conversions still checked).Tests
ValidatorIndex(0),ValidatorIndex(1), andValidatorIndex(u64::MAX).PartialSignatureMessagesencode round-trip asserting the 164-byte layout.u64(whose impls are not cfg-gated); a 32-bitcargo checkin CI is optional reinforcement.Notes
ssv_typeswas introduced (Newssv_types#69); it was surfaced by a wire-compatibility comparison against go-ssv and ssv-spec.ssv_typesPR. Because 64-bit bytes are unchanged, it neither blocks nor is blocked by in-flight work touching these containers (e.g. feat: add Role::ProposerPreferences with message-validator fork-gate #1106, feat(validator_store): implement sign_proposer_preferences (SingleValidator partial-sig) #1125) and can merge in any order.unstableat3a414432.Issues are directionally correct, not prescriptive; verify symbols at PR time.