Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EIP6110: add validator index cache #13943

Merged
merged 3 commits into from
May 2, 2024
Merged

Conversation

terencechain
Copy link
Member

@terencechain terencechain commented Apr 30, 2024

This PR introduces a new validator index that complies with EIP-6110, specifically adhering to the validator index invariant constraint detailed in this section. Below are the design considerations:

  • The cache is located within the native-state package adjacent to its usage at (b *BeaconState) ValidatorIndexByPubkey.

  • The cache is shared among all BeaconState instances. When a state is copied, only the reference to the cache is copied.

  • The cache maintains a map of public key to index and includes a lock mechanism.

  • A feature flag eip6110-validator-cache has been introduced to enable risk-free merging into the develop branch and facilitate testing, allowing easy toggling on and off as needed.

  • A new beacon state helper getter, validatorsReadOnlySinceIndex(index int), has been implemented. This helper enables the state to return an array of read only validator pointers after the specified index. After reviewing all beacon state helpers for retrieving validators, none proved sufficient for the following reasons:

    • ValidatorsReadOnly() initializes every validator, significantly increasing heap usage.
    • PublicKeys() copies all public keys across the validator set.
    • PubkeyAtIndex(idx primitives.ValidatorIndex) requires frequent locking and unlocking, which is suboptimal in the validator loop.
    • ReadFromEveryValidator(f func(idx int, val ReadOnlyValidator) error) also necessitates frequent lock changes for similar reasons as above.
    • PublicKeySlice is added for read only validator to return public key slice without copying to 48 bytes
  • For the read usage side, nothing needs to be done. For the write usage side, we need to call SaveValidatorIndices() when a new finalized state is emitted and when the node starts up. This part is TBD

@terencechain terencechain added the Electra electra hardfork label Apr 30, 2024
@terencechain terencechain requested a review from a team as a code owner April 30, 2024 22:11
@terencechain terencechain force-pushed the eip6110-validator-cache branch 2 times, most recently from 9ccd63c to 25034dd Compare April 30, 2024 22:13
// publicKeysSinceIndexReadOnly constructs a list of validator public keys starting from a specified index.
// The indices in the returned list correspond to their respective validator indices in the state.
// It returns an error if the specified index is out of bounds. This function is read-only and does not use locks.
func (b *BeaconState) publicKeysSinceIndexReadOnly(index int) ([][fieldparams.BLSPubkeyLength]byte, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I was going to say it would be better for this to return a slice of validators, which would be super cheap if we had normal slices, but I suppose mv slice messes that up. Can mv slice return a go slice without making a bunch of copies?

Even it if has to copy validator pointers, that's still going to be a lot less than copying all the keys.

Another option is to design this as a method like findValidator that takes a callback ("query") and iterates through the validators until the callback returns true, then return the matching validator. Or similar idea but it iterates until the callback tells it to stop, and then the callback grabs the validator it wants via enclosure.

Copy link
Member Author

Choose a reason for hiding this comment

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

Re: pointer, there is ValidatorsReadOnly() []state.ReadOnlyValidator which returns a list of

type readOnlyValidator struct {
	validator *ethpb.Validator
}

Even then, 1M read-only validators (pointers) seem to be a lot for every deposit. I think it'd still make sense to implement ValidatorsSinceIndex(index int). Do you agree with that?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes I thought the point about only iterating validators since the index was implied - ValidatorsSinceIndex(index int) is actually what I had in mind :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks. Updated the code to do that : )
Unfortunately, the read-only validator public key getter also requires a copy. I added a slice version

func (v readOnlyValidator) PublicKey() [fieldparams.BLSPubkeyLength]byte {
	var pubkey [fieldparams.BLSPubkeyLength]byte
	copy(pubkey[:], v.validator.PublicKey)
	return pubkey
}

type validatorIndexCache struct {
indexMap map[[fieldparams.BLSPubkeyLength]byte]primitives.ValidatorIndex // Maps BLS public keys to validator indices.
lastFinalizedIndex int // Index of the last finalized validator to avoid reading validators before this point.
mutex sync.RWMutex
Copy link
Contributor

Choose a reason for hiding this comment

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

removing the field name will allow you to write validatorIndexCache.Lock() directly

}

finalizedIndex := b.validatorIndexCache.lastFinalizedIndex
pubKeys, err := b.publicKeysSinceIndexReadOnly(finalizedIndex)
Copy link
Contributor

Choose a reason for hiding this comment

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

This will also return the last finalized validator, which is not a bug but also not exactly what we want. The "correct" thing to do is to call the function with finalizedIndex + 1 and later write index := primitives.ValidatorIndex(finalizedIndex + i + 1). Similarly for saveValidatorIndices.

Copy link
Member Author

@terencechain terencechain May 1, 2024

Choose a reason for hiding this comment

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

I think it's better to update publicKeysSinceIndexReadOnly to return i+1. Example:
[0,1,2,3,4], where input 2 should return [3,4], not [2,3,4]

I ended up removing the index and just used len(indexMap) to serve the same purpose. It's cleaner

// validatorIndexCache maintains a mapping from validator public keys to their indices within the beacon state.
// It includes a lastFinalizedIndex to track updates up to the last finalized validator index,
// and uses a mutex for concurrent read/write access to the cache.
type validatorIndexCache struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should track only finalized validators, so I would rename it to finalizedValidatorIndexCache

@terencechain terencechain force-pushed the eip6110-validator-cache branch 2 times, most recently from 24281d5 to 02e51f0 Compare May 1, 2024 15:11
@terencechain terencechain added the Ready For Review A pull request ready for code review label May 1, 2024
@terencechain terencechain force-pushed the eip6110-validator-cache branch from 02e51f0 to 7f77dc5 Compare May 2, 2024 00:25
@@ -110,6 +111,7 @@ type ReadOnlyValidator interface {
WithdrawableEpoch() primitives.Epoch
ExitEpoch() primitives.Epoch
PublicKey() [fieldparams.BLSPubkeyLength]byte
PublicKeySlice() []byte
Copy link
Contributor

Choose a reason for hiding this comment

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

This function can be unexported

{PublicKey: []byte{2}},
{PublicKey: []byte{3}},
}
// We should get able to retrieve these validators by public key even they are not in the cache
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// We should get able to retrieve these validators by public key even they are not in the cache
// We should get able to retrieve these validators by public key even when they are not in the cache

{PublicKey: []byte{4}},
{PublicKey: []byte{5}},
}
// We should get able to retrieve these validators by public key even they are not in the cache
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// We should get able to retrieve these validators by public key even they are not in the cache
// We should be able to retrieve these validators by public key even when they are not in the cache

{PublicKey: []byte{5}},
{PublicKey: []byte{6}},
}
// We should get able to retrieve these validators by public key even they are not in the cache
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// We should get able to retrieve these validators by public key even they are not in the cache
// We should be able to retrieve these validators by public key even when they are not in the cache

"github.com/prysmaticlabs/prysm/v5/testing/require"
)

func Test_FinalizedValidatorIndexCache(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

One additional thing you might want to check is retrieving a saved index

require.Equal(t, primitives.ValidatorIndex(2), i)
require.Equal(t, true, exists)

// State is finalized. We flush [0, 1, 2 ] to the cache.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// State is finalized. We flush [0, 1, 2 ] to the cache.
// State is finalized. We save [0, 1, 2] to the cache.

This is nitpicky, but technically "flush" would mean these indices are no longer present in the state. Someone reading the test might get the wrong impression of what the code does.

require.Equal(t, primitives.ValidatorIndex(4), i)
require.Equal(t, true, exists)

// State is finalized. We flush [4, 5] to the cache.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// State is finalized. We flush [4, 5] to the cache.
// State is finalized. We save [4, 5] to the cache.

@terencechain terencechain added this pull request to the merge queue May 2, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to no response for status checks May 2, 2024
@terencechain terencechain added this pull request to the merge queue May 2, 2024
Merged via the queue into develop with commit 625818d May 2, 2024
16 of 17 checks passed
@terencechain terencechain deleted the eip6110-validator-cache branch May 2, 2024 17:17
nisdas pushed a commit that referenced this pull request Jul 4, 2024
* EIP6110: add validator index cache

* Add tests

* Radek's feedback
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Electra electra hardfork Ready For Review A pull request ready for code review
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants