Skip to content

Commit

Permalink
eip-7251: process_registry_updates (#14005)
Browse files Browse the repository at this point in the history
* eip-7251: registry_updates

* Fix spectest violation

* More unit tests for process_registry_updates

* Update tests for ProcessRegistryUpdates

* Minor refactorings and benchmark

* Update beacon-chain/core/electra/registry_updates.go

Co-authored-by: Sammy Rosso <15244892+saolyn@users.noreply.github.com>

* Wrap errors from process_registry_updates for easier debugging

* Rewrite process_registry_updates not to use st.Validators()

---------

Co-authored-by: Sammy Rosso <15244892+saolyn@users.noreply.github.com>
  • Loading branch information
prestonvanloon and saolyn authored Jun 29, 2024
1 parent 028504a commit 041e81a
Show file tree
Hide file tree
Showing 10 changed files with 280 additions and 7 deletions.
1 change: 1 addition & 0 deletions beacon-chain/core/electra/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ go_test(
"deposit_fuzz_test.go",
"deposits_test.go",
"effective_balance_updates_test.go",
"registry_updates_test.go",
"transition_test.go",
"upgrade_test.go",
"validator_test.go",
Expand Down
81 changes: 76 additions & 5 deletions beacon-chain/core/electra/registry_updates.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ package electra

import (
"context"
"fmt"

"github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/core/time"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/core/validators"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/state"
"github.com/prysmaticlabs/prysm/v5/config/params"
"github.com/prysmaticlabs/prysm/v5/consensus-types/primitives"
)

// ProcessRegistryUpdates rotates validators in and out of active pool.
// the amount to rotate is determined churn limit.
// ProcessRegistryUpdates processes all validators eligible for the activation queue, all validators
// which should be ejected, and all validators which are eligible for activation from the queue.
//
// Spec pseudocode definition:
//
Expand All @@ -28,7 +34,72 @@ import (
// for validator in state.validators:
// if is_eligible_for_activation(state, validator):
// validator.activation_epoch = activation_epoch
func ProcessRegistryUpdates(ctx context.Context, state state.BeaconState) (state.BeaconState, error) {
// TODO: replace with real implementation
return state, nil
func ProcessRegistryUpdates(ctx context.Context, st state.BeaconState) error {
currentEpoch := time.CurrentEpoch(st)
ejectionBal := params.BeaconConfig().EjectionBalance
activationEpoch := helpers.ActivationExitEpoch(currentEpoch)

// To avoid copying the state validator set via st.Validators(), we will perform a read only pass
// over the validator set while collecting validator indices where the validator copy is actually
// necessary, then we will process these operations.
eligibleForActivationQ := make([]primitives.ValidatorIndex, 0)
eligibleForEjection := make([]primitives.ValidatorIndex, 0)
eligibleForActivation := make([]primitives.ValidatorIndex, 0)

if err := st.ReadFromEveryValidator(func(idx int, val state.ReadOnlyValidator) error {
// Collect validators eligible to enter the activation queue.
if helpers.IsEligibleForActivationQueue(val, currentEpoch) {
eligibleForActivationQ = append(eligibleForActivationQ, primitives.ValidatorIndex(idx))
}

// Collect validators to eject.
if val.EffectiveBalance() <= ejectionBal && helpers.IsActiveValidatorUsingTrie(val, currentEpoch) {
eligibleForEjection = append(eligibleForEjection, primitives.ValidatorIndex(idx))
}

// Collect validators eligible for activation and not yet dequeued for activation.
if helpers.IsEligibleForActivationUsingTrie(st, val) {
eligibleForActivation = append(eligibleForActivation, primitives.ValidatorIndex(idx))
}

return nil
}); err != nil {
return fmt.Errorf("failed to read validators: %w", err)
}

// Handle validators eligible to join the activation queue.
for _, idx := range eligibleForActivationQ {
v, err := st.ValidatorAtIndex(idx)
if err != nil {
return err
}
v.ActivationEligibilityEpoch = currentEpoch + 1
if err := st.UpdateValidatorAtIndex(idx, v); err != nil {
return fmt.Errorf("failed to updated eligible validator at index %d: %w", idx, err)
}
}

// Handle validator ejections.
for _, idx := range eligibleForEjection {
var err error
// exitQueueEpoch and churn arguments are not used in electra.
st, _, err = validators.InitiateValidatorExit(ctx, st, idx, 0 /*exitQueueEpoch*/, 0 /*churn*/)
if err != nil {
return fmt.Errorf("failed to initiate validator exit at index %d: %w", idx, err)
}
}

for _, idx := range eligibleForActivation {
// Activate all eligible validators.
v, err := st.ValidatorAtIndex(idx)
if err != nil {
return err
}
v.ActivationEpoch = activationEpoch
if err := st.UpdateValidatorAtIndex(idx, v); err != nil {
return fmt.Errorf("failed to activate validator at index %d: %w", idx, err)
}
}

return nil
}
145 changes: 145 additions & 0 deletions beacon-chain/core/electra/registry_updates_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package electra_test

import (
"context"
"testing"

"github.com/prysmaticlabs/prysm/v5/beacon-chain/core/electra"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/state"
state_native "github.com/prysmaticlabs/prysm/v5/beacon-chain/state/state-native"
fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams"
"github.com/prysmaticlabs/prysm/v5/config/params"
"github.com/prysmaticlabs/prysm/v5/consensus-types/primitives"
eth "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v5/testing/require"
"github.com/prysmaticlabs/prysm/v5/testing/util"
)

func TestProcessRegistryUpdates(t *testing.T) {
finalizedEpoch := primitives.Epoch(4)

tests := []struct {
name string
state state.BeaconState
check func(*testing.T, state.BeaconState)
}{
{
name: "No rotation",
state: func() state.BeaconState {
base := &eth.BeaconStateElectra{
Slot: 5 * params.BeaconConfig().SlotsPerEpoch,
Validators: []*eth.Validator{
{ExitEpoch: params.BeaconConfig().MaxSeedLookahead},
{ExitEpoch: params.BeaconConfig().MaxSeedLookahead},
},
Balances: []uint64{
params.BeaconConfig().MaxEffectiveBalance,
params.BeaconConfig().MaxEffectiveBalance,
},
FinalizedCheckpoint: &eth.Checkpoint{Root: make([]byte, fieldparams.RootLength)},
}
st, err := state_native.InitializeFromProtoElectra(base)
require.NoError(t, err)
return st
}(),
check: func(t *testing.T, st state.BeaconState) {
for i, val := range st.Validators() {
require.Equal(t, params.BeaconConfig().MaxSeedLookahead, val.ExitEpoch, "validator updated unexpectedly at index %d", i)
}
},
},
{
name: "Validators are activated",
state: func() state.BeaconState {
base := &eth.BeaconStateElectra{
Slot: 5 * params.BeaconConfig().SlotsPerEpoch,
FinalizedCheckpoint: &eth.Checkpoint{Epoch: finalizedEpoch, Root: make([]byte, fieldparams.RootLength)},
}
for i := uint64(0); i < 10; i++ {
base.Validators = append(base.Validators, &eth.Validator{
ActivationEligibilityEpoch: finalizedEpoch,
EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance,
ActivationEpoch: params.BeaconConfig().FarFutureEpoch,
})
}
st, err := state_native.InitializeFromProtoElectra(base)
require.NoError(t, err)
return st
}(),
check: func(t *testing.T, st state.BeaconState) {
activationEpoch := helpers.ActivationExitEpoch(5)
// All validators should be activated.
for i, val := range st.Validators() {
require.Equal(t, activationEpoch, val.ActivationEpoch, "failed to update validator at index %d", i)
}
},
},
{
name: "Validators are exited",
state: func() state.BeaconState {
base := &eth.BeaconStateElectra{
Slot: 5 * params.BeaconConfig().SlotsPerEpoch,
FinalizedCheckpoint: &eth.Checkpoint{Epoch: finalizedEpoch, Root: make([]byte, fieldparams.RootLength)},
}
for i := uint64(0); i < 10; i++ {
base.Validators = append(base.Validators, &eth.Validator{
EffectiveBalance: params.BeaconConfig().EjectionBalance - 1,
ExitEpoch: params.BeaconConfig().FarFutureEpoch,
WithdrawableEpoch: params.BeaconConfig().FarFutureEpoch,
})
}
st, err := state_native.InitializeFromProtoElectra(base)
require.NoError(t, err)
return st
}(),
check: func(t *testing.T, st state.BeaconState) {
// All validators should be exited
for i, val := range st.Validators() {
require.NotEqual(t, params.BeaconConfig().FarFutureEpoch, val.ExitEpoch, "failed to update exit epoch on validator %d", i)
require.NotEqual(t, params.BeaconConfig().FarFutureEpoch, val.WithdrawableEpoch, "failed to update withdrawable epoch on validator %d", i)
}
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := electra.ProcessRegistryUpdates(context.TODO(), tt.state)
require.NoError(t, err)
if tt.check != nil {
tt.check(t, tt.state)
}
})
}
}

func Benchmark_ProcessRegistryUpdates_MassEjection(b *testing.B) {
bal := params.BeaconConfig().EjectionBalance - 1
ffe := params.BeaconConfig().FarFutureEpoch
genValidators := func(num uint64) []*eth.Validator {
vals := make([]*eth.Validator, num)
for i := range vals {
vals[i] = &eth.Validator{
EffectiveBalance: bal,
ExitEpoch: ffe,
}
}
return vals
}

st, err := util.NewBeaconStateElectra()
require.NoError(b, err)

for i := 0; i < b.N; i++ {
b.StopTimer()
if err := st.SetValidators(genValidators(100000)); err != nil {
panic(err)
}
b.StartTimer()

if err := electra.ProcessRegistryUpdates(context.TODO(), st); err != nil {
panic(err)
}
}
}
3 changes: 1 addition & 2 deletions beacon-chain/core/electra/transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ func ProcessEpoch(ctx context.Context, state state.BeaconState) error {
return errors.Wrap(err, "could not process rewards and penalties")
}

state, err = ProcessRegistryUpdates(ctx, state)
if err != nil {
if err := ProcessRegistryUpdates(ctx, state); err != nil {
return errors.Wrap(err, "could not process registry updates")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ go_test(
"pending_balance_updates_test.go",
"pending_consolidations_test.go",
"randao_mixes_reset_test.go",
"registry_updates_test.go",
"rewards_and_penalties_test.go",
"slashings_reset_test.go",
"slashings_test.go",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package epoch_processing

import (
"testing"

"github.com/prysmaticlabs/prysm/v5/testing/spectest/shared/electra/epoch_processing"
)

func TestMainnet_Electra_EpochProcessing_RegistryUpdates(t *testing.T) {
epoch_processing.RunRegistryUpdatesTests(t, "mainnet")
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ go_test(
"pending_balance_updates_test.go",
"pending_consolidations_test.go",
"randao_mixes_reset_test.go",
"registry_updates_test.go",
"rewards_and_penalties_test.go",
"slashings_reset_test.go",
"slashings_test.go",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package epoch_processing

import (
"testing"

"github.com/prysmaticlabs/prysm/v5/testing/spectest/shared/electra/epoch_processing"
)

func TestMinimal_Electra_EpochProcessing_RegistryUpdates(t *testing.T) {
epoch_processing.RunRegistryUpdatesTests(t, "minimal")
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ go_library(
"pending_balance_updates.go",
"pending_consolidations.go",
"randao_mixes_reset.go",
"registry_updates.go",
"rewards_and_penalties.go",
"slashings.go",
"slashings_reset.go",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package epoch_processing

import (
"context"
"path"
"testing"

"github.com/prysmaticlabs/prysm/v5/beacon-chain/core/electra"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers"
"github.com/prysmaticlabs/prysm/v5/beacon-chain/state"
"github.com/prysmaticlabs/prysm/v5/testing/require"
"github.com/prysmaticlabs/prysm/v5/testing/spectest/utils"
)

// RunRegistryUpdatesTests executes "epoch_processing/registry_updates" tests.
func RunRegistryUpdatesTests(t *testing.T, config string) {
require.NoError(t, utils.SetConfig(t, config))

testFolders, testsFolderPath := utils.TestFolders(t, config, "electra", "epoch_processing/registry_updates/pyspec_tests")
for _, folder := range testFolders {
t.Run(folder.Name(), func(t *testing.T) {
// Important to clear cache for every test or else the old value of active validator count gets reused.
helpers.ClearCache()
folderPath := path.Join(testsFolderPath, folder.Name())
RunEpochOperationTest(t, folderPath, processRegistryUpdatesWrapper)
})
}
}

func processRegistryUpdatesWrapper(_ *testing.T, state state.BeaconState) (state.BeaconState, error) {
return state, electra.ProcessRegistryUpdates(context.Background(), state)
}

0 comments on commit 041e81a

Please sign in to comment.