Skip to content

Optimize beacon API (REST and gRPC) regarding endpoints needing to iterate over the whole validator set.#16838

Merged
nalepae merged 13 commits into
developfrom
func-iterator
May 27, 2026
Merged

Optimize beacon API (REST and gRPC) regarding endpoints needing to iterate over the whole validator set.#16838
nalepae merged 13 commits into
developfrom
func-iterator

Conversation

@nalepae

@nalepae nalepae commented May 24, 2026

Copy link
Copy Markdown
Contributor

What type of PR is this?
Bug fix and optimization

What does this PR do? Why is it needed?
Some of the beacon endpoints currently wastefully materialize the entire validator set (~2.3M validators on mainnet) with the state.Validators function before filtering interesting validators. The call to state.Validators is not free at all, especially on mainnet, and, because Ethereum doesn't currently have validator index re-uses, this call will become more and more expensive to do.

This PR implements the "yield" iterations for these endpoints, allowing them to filter "on the flight" validator they want to keep and thus preventing wasteful costly heap allocation.

Notable example of gain: /eth/v1/beacon/states/{state_id}/validators?status=withdrawal_possible.
Previously, all the validators where materialized in the node heap (~2.3M on mainnet). With this PR, only the exited slashed validators are materialized (1442 on mainnet).

This pull requests:

  • Fixes the listed issue
  • Applies the optimization avoiding materializing the whole validator set (~2.3M validators on mainnet) when possible, thanks to the function iterator golang feature.

Which issues(s) does this PR fix?

Other notes for review

Important

Please read commit by commit, with commit messages, since they contain useful information to understand the code changes.

Note

Two calls to ValidatorsReadOnly remain in hdiff. It may probably be interesting to remove them, but it will be done (if needed) in an other PR.
EDIT: (Seen with @potuz on Prysm internal messaging system: Will be done in a next PR.)

Acknowledgements

  • I have read CONTRIBUTING.md.
  • I have included a uniquely named changelog fragment file.
  • I have added a description with sufficient context for reviewers to understand this PR.
  • I have tested that my changes work as expected and I added a testing plan to the PR description (if applicable).

Previous implementation was returning ALL active validators, contrary to what is stated by the godoc.
…f an `int`.

Rationale:
Most of callers had to do this cast. Now the cast is directly done in the function, which is simpler for callers.
@nalepae nalepae changed the title ReadFromEveryValidator==> AllValidatorsReadOnly(func iterator) Optimize beacon API (REST and gRPC) regarding endpoints needing to iterate over the validator set. May 25, 2026
@nalepae nalepae changed the title Optimize beacon API (REST and gRPC) regarding endpoints needing to iterate over the validator set. Optimize beacon API (REST and gRPC) regarding endpoints needing to iterate over the whole validator set. May 25, 2026
nalepae added 7 commits May 25, 2026 15:39
This (very slightly) adds some complexity to the implementation of `ValidatorsReadOnlySeq` (which is done only once),
but it simplifies the call sites (which are numerous).
…dators (mainnet) heap allocation by using `requestedState.ValidatorsReadOnlySeq` instead of `requestedState.Validators`.
…over validators as an input instead of the full validators slice.

It avoids a ~2.3M validators (mainnet) materialization.
…): Avoid full validators list (2.3M on mainnet) materialization.
…e): Avoid the full validators list (~2.3M on mainnet) materialization.
@nalepae nalepae marked this pull request as ready for review May 25, 2026 14:09

@potuz potuz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

bal.ActiveCurrentEpoch, err = math.Add64(bal.ActiveCurrentEpoch, val.EffectiveBalance())
if err != nil {
return err
return nil, nil, fmt.Errorf("add 64: %w", err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess Claude did not understand what the function Add64 does :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha no it was not claude but me.
I always follow this convention:
https://github.com/uber-go/guide/blob/master/style.md#error-wrapping

But yes in this case in not so clear. Will fix

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 817ff4d.

bal.ActivePrevEpoch, err = math.Add64(bal.ActivePrevEpoch, val.EffectiveBalance())
if err != nil {
return err
return nil, nil, fmt.Errorf("add 64: %w", err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same, perhaps "overflow" or " Add64" would be better.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 817ff4d.

require.NoError(t, err)
_, _, err = InitializePrecomputeValidators(t.Context(), s)
require.ErrorContains(t, "could not read every validator: addition overflows", err)
require.ErrorContains(t, "add 64: addition overflows", err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

interesting, this one actually added the "overflow" statement.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 817ff4d.

Comment thread beacon-chain/rpc/core/validator.go Outdated
Err: errors.Wrap(err, "could not determine exited validator indices"),
Reason: Internal,
var (
activatedIndices, exitedIndices, slashedIndices, ejectedIndices []primitives.ValidatorIndex

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An only nit is that these variables are not initialized so if no val is activated for example, this function will return nil when before it would return an initialized empty slice. This is fine as far as I can see in the current code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6039980.

slashingsVector := params.BeaconConfig().EpochsPerSlashingsVector

for idx, validator := range requestedState.ValidatorsReadOnlySeq() {
if validator.ActivationEpoch() == requestedEpoch {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes the linked issue, but the test TestActivatedValidatorIndices was removed, it would be nice to add a regression unit test that actually tests without a request epoch being zero so that we can assert that the indices were not in the reply (the previous test covered this case but asserted the opposite).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6039980.


for i := range b.validatorsMultiValue.Len(b) {
v, err := b.validatorsMultiValue.At(b, uint64(i))
if err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we not need to log errors for this stuff? or does that just clutter?

@nalepae nalepae May 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case is basically impossible, since the only way At returns on error is in case we have an out of bound error.

At is between 0 and Len(b), and we have a lock at the start of the function so no mutation of validatorsMultiValue is possible ==> No out of bound error can happen.

Added a log however in 3245eb5.

@james-prysm james-prysm left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@nalepae nalepae added this pull request to the merge queue May 27, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks May 27, 2026
@nalepae nalepae added this pull request to the merge queue May 27, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks May 27, 2026
@nalepae nalepae added this pull request to the merge queue May 27, 2026
Merged via the queue into develop with commit 4731880 May 27, 2026
23 checks passed
@nalepae nalepae deleted the func-iterator branch May 27, 2026 12:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants