Add SSZ response support for validators and validator_balances endpoints#601
Add SSZ response support for validators and validator_balances endpoints#601Zyra-V21 wants to merge 1 commit into
Conversation
Adds `application/octet-stream` (SSZ) response type to:
- `GET /eth/v1/beacon/states/{state_id}/validators`
- `POST /eth/v1/beacon/states/{state_id}/validators`
- `GET /eth/v1/beacon/states/{state_id}/validators/{validator_id}`
- `GET /eth/v1/beacon/states/{state_id}/validator_balances`
- `POST /eth/v1/beacon/states/{state_id}/validator_balances`
The validators response uses the SSZ container
`{ index: uint64, balance: uint64, status: byte, validator: Validator }`,
where `status` is encoded as a single `byte` per the table now documented in
the `ValidatorStatus` schema. Mapping follows the proposal in
ethereum#333 (0x01..0x09, with 0x00 reserved for unknown).
Adds 406 responses for unsupported `Accept` media types and a CHANGES.md
entry under the development version.
Closes ethereum#333.
| SSZ encoding (single `byte`, used in `application/octet-stream` responses): | ||
| | Value | Status | | ||
| | ------ | --------------------- | | ||
| | `0x00` | unknown | | ||
| | `0x01` | pending_initialized | | ||
| | `0x02` | pending_queued | | ||
| | `0x03` | active_ongoing | | ||
| | `0x04` | active_exiting | | ||
| | `0x05` | active_slashed | | ||
| | `0x06` | exited_unslashed | | ||
| | `0x07` | exited_slashed | | ||
| | `0x08` | withdrawal_possible | | ||
| | `0x09` | withdrawal_done | | ||
|
|
There was a problem hiding this comment.
mmh, would be cleaner to defer until SSZ receives proper enum support.
until then, would align validators/builders endpoints to match the same form (and keep JSON-only for now).
SSZ support doesn't need to be coordinated around a hardfork, client can indicate preferences via Accept header and server can just start shipping SSZ response once we decided on how to tackle enums.
There was a problem hiding this comment.
Hey @etan-status, thanks for taking a look 🙏
I think we might actually be on the same page already: the PR doesn't use an SSZ enum type for status. It's encoded as a plain byte with the mapping just documented in the schema, exactly because @arnetheduck / @michaelsproul pushed back on uint8 in #333 to avoid implying any numeric/arithmetic semantics. So in that sense it's already "not depending on proper SSZ enum support" -> it's an opaque byte we agreed on out-of-band, not something the SSZ spec has to model.
So I want to make sure I understand what you'd prefer:
- keep the opaque byte as-is (no SSZ enum machinery involved), or
- drop status from the SSZ payload entirely for now and revisit once there's a proper enum story, or
- hold the whole validators SSZ response as JSON-only until then?
I'd push back a bit on (3) : the main driver here is that the full validator set is ~1 GB of JSON per state on mainnet, which is a real bottleneck for indexers like goteth, so getting SSZ out is kind of the whole point. The byte was meant to be the minimal, non-controversial way to carry status without waiting on an enum decision.
One thing that might help unblock us regardless: validator_balances is just { index: uint64, balance: uint64 } : no enum, no status, nothing to defer. Happy to split that out so it can land independently while we settle the status question on the validators side.
And yeah, fully agree the validators/builders endpoints should end up consistent ; I'll follow #610/#614 so whatever we land on for enums applies the same way across both. Lmk which direction you'd like and I'll adjust.
There was a problem hiding this comment.
I would prefer (3) because (1) would mean we add the format as proposed here into the spec, if the enum ssz type has a different byte mapping, it would be a breaking change and we need to do a v2, there is also a cost to implementing this is in a non-standard way, fun fact, I did this almost 3 years ago here ChainSafe/lodestar#6059 but we never shipped it. But there was a lodestar branch that was able to return validators response in ssz format here ChainSafe/lodestar#6060 and it worked. But even after almost 3 years, I don't think the solution as proposed in this PR is correct as much as I want the validator response to be ssz encoded. And lastly, regarding (2), this is obviously a breaking change, and not in favor of this, we discussed removing status before (many times actually) and there was never a rough consensus on it.
There was a problem hiding this comment.
Also @Zyra-V21 as a side note, can you please stop using claude to comment for you, it's fine to use AI to generate code, but responding to comments and forcing others to read through the AI slop I find personally a bit direspectul if I didn't know you this PR would be closed already.
There was a problem hiding this comment.
quick note on provenance: the 0x00 unknown … 0x09 withdrawal_done mapping here isn't new, its @mcdee's from #333, and its the same byte encoding your own ChainSafe/lodestar#6059 used, with an explicit "update to canonical once the spec defines it" todo. that's basically the posture im proposing too: ship the agreed byte form now, align if/when a canonical enum lands.
and that "if/when" is the whole point. the canonical-json work this waits on (@arnetheduck's consensus-specs#2983) was open from 2022 and closed unmerged in jan 2025. so (3) isn't "defer briefly", its json-only indefinitely, for a ~1 gb/state payload. the ssz form is opt-in via accept and purely additive, so if a canonical encoding ever supersedes this byte it's a versioning question for an opt-in content type, not a break to anything shipping today.
couple of concrete things: can validator_balances land now? it's just { index, balance }, no status, no enum, nothing your concern touches. and for validators, tell me the exact encoding you'd accept and i'll match it and align it with the builders endpoints (#610/#614) so we don't fragment this.
p.s: slop means low-quality, mass-produced content. this is a single sourced comment, and it's accurate, show me one wrong line if it isn't. saying a technically-sound pr would already be closed over how it's written rather than what it argues is shooting the messenger. let's stay on the encoding.
There was a problem hiding this comment.
adding ssz to the validator_balances is a no brainer, @lodekeeper can you check lodestar, we should support this since ~2 years already, it makes sense to document that in the spec, it's also backward compatible to add ssz to an existing endpoint (due to content-type negotiation) so this seems overdue
There was a problem hiding this comment.
Confirmed — Lodestar already supports SSZ for validator_balances.
GET /eth/v1/beacon/states/{state_id}/validator_balances has no onlySupport: json restriction in its route definition, so it does content-type negotiation and returns SSZ on Accept: application/octet-stream. The encoding is List[Container{index: uint64, balance: uint64}] (ValidatorBalanceListType) — which matches the shape proposed here exactly, so documenting it just standardizes what we already serve.
It's been in since #6749 ("provide first-class ssz support on api", merged 2024-06-10), shipped in v1.20.0 — so ~2 years, as you remembered.
For contrast, validators (getStateValidators/postStateValidators) is explicitly onlySupport: WireFormat.json on our side — JSON-only — which lines up with leaving that endpoint at status quo per the earlier discussion.
So +1 to documenting the validator_balances SSZ format in the spec — it's backward-compatible (Accept-header opt-in) and already implemented.
There was a problem hiding this comment.
I'll take a read at everything carefully and will be back by Monday! Thanks everyone for your collaboration, have a good weekend
There was a problem hiding this comment.
thanks all, just went through all the comments above and im happy where this landed
i'll pull validator_balances out into its own pr. since @lodekeeper confirmed lodestar has been serving ssz on it since #6749 (v1.20.0) with the same {index, balance} shape, this is basically just writing down what's already shipping. opt in via the accept header so nothing changes for json clients.
fwiw on @etan-status's point about whether anyone actually struggles with the json size vs just assuming it: that's literally us (@migalabs) . this is the exact spot goteth hits it. we snapshot balances every epoch and backfill big historical ranges, and at full set size it's the json encode/parse on both ends that hurts, not really the wire bytes (gzip helps there, but not with the cpu cost). ssz makes wide backfills a lot cheaper.
and yeah, fine to leave validators at status quo for now. i didn't realise #449 had already tried the v2-without-status shape and couldn't get consensus, so no point re-running that here. if ssz for validators ever happens it should go through the proper enum process like @etan-status described, and stay consistent with builders. happy to help on that when it's a thing.
thanks @nflaig @lodekeeper @etan-status for working through it
There was a problem hiding this comment.
Sounds good 👍 The concrete goteth/migalabs angle (encode/parse CPU on epoch-by-epoch balance backfills, not just wire bytes) is a useful data point to have on record. And since Lodestar already serves the {index, balance} SSZ shape, the split PR should be pure spec documentation — nothing new to implement on our side. I'll keep an eye out for it and happy to review. Thanks all.
Motivation
The
/eth/v1/beacon/states/{state_id}/validatorsendpoint currently returns JSON only. On mainnet today this is roughly 1 GB per state for the full validator set, while the equivalent SSZ payload is around an order of magnitude smaller and trivially cheaper to serialize and parse on both sides. The same applies tovalidator_balances(around 85 MB JSON per state).For analytics and indexing tooling that snapshots validators frequently — for example Migalabs' goteth and similar consensus-layer indexers — JSON parsing and beacon-node CPU time during state snapshots are a real bottleneck. Exposing SSZ on these endpoints reduces both ends of the cost.
This proposal does not introduce a new design: it implements the agreement reached in the discussion of #333 (which has been open and unactioned since 2023).
Scope
Adds
application/octet-stream(SSZ) response support to:GET /eth/v1/beacon/states/{state_id}/validatorsPOST /eth/v1/beacon/states/{state_id}/validatorsGET /eth/v1/beacon/states/{state_id}/validators/{validator_id}GET /eth/v1/beacon/states/{state_id}/validator_balancesPOST /eth/v1/beacon/states/{state_id}/validator_balancesEach affected endpoint also gains a
406 Not Acceptableresponse, matching the pattern used by other SSZ-enabled endpoints in the spec (debug/state.v2,pending_partial_withdrawals,validator_identities, etc.).Validator status encoding
ValidatorResponseis wire-encoded as the SSZ container:Validatoris the consensus-spec object.statusis a singlebyte. The byte mapping is documented in theValidatorStatusschema and is the one proposed by @mcdee in #333:0x000x010x020x030x040x050x060x070x080x09byteis used rather thanuint8per the discussion in #333 (cc @arnetheduck, @michaelsproul) to avoid implying the status is a numeric value with arithmetic semantics.ValidatorBalanceResponseSSZ encoding is the simpler{ index: uint64, balance: uint64 }container; no new type definitions are required.What this PR does not change
application/octet-streamcontinue to receive the same JSON they get today.ValidatorStatusJSON enum (active_ongoing, etc.) remains the canonical string form. The byte mapping is only relevant on the SSZ wire.Lint
redocly lint beacon-node-oapi.yamlreports the same baseline of pre-existing warnings/errors before and after this patch (84 errors, 15 warnings, all unrelated to the touched files). No new lint findings are introduced.Background and credits
The endpoint design, the choice of
byteoveruint8, and the status mapping all come from the discussion in #333. This PR is the implementation of that agreement; the credit for the design belongs to:bytevsuint8clarification.Validatorobject.validator_balanceswas added because it shares the same indexer use case and avoids leaving an obviously asymmetric gap.Related prior art that did not result in a merged spec change:
validatorsandvalidator_balances#367 added POST variants for these endpoints but kept JSON-only responses; the SSZ side was left open.Closes #333.