Compare commits

...

8 Commits

Author SHA1 Message Date
terence tsao
48d7e9c292 process registry updates using read from every validator 2024-06-18 17:01:59 -07:00
Preston Van Loon
8a1b0e2c4c Wrap errors from process_registry_updates for easier debugging 2024-06-18 11:53:56 -05:00
Preston Van Loon
7685a0d2f7 Update beacon-chain/core/electra/registry_updates.go
Co-authored-by: Sammy Rosso <15244892+saolyn@users.noreply.github.com>
2024-06-18 11:50:43 -05:00
Preston Van Loon
8fa05b7f69 Minor refactorings and benchmark 2024-06-17 20:22:19 -05:00
Preston Van Loon
8a6b73c0fd Update tests for ProcessRegistryUpdates 2024-06-17 16:20:53 -05:00
Preston Van Loon
c5d4193bb4 More unit tests for process_registry_updates 2024-06-16 21:02:19 -05:00
Preston Van Loon
49290fd2d6 Fix spectest violation 2024-06-16 21:02:17 -05:00
Preston Van Loon
414b8ace75 eip-7251: registry_updates 2024-06-16 21:02:17 -05:00
11 changed files with 281 additions and 14 deletions

View File

@@ -48,6 +48,7 @@ go_test(
"consolidations_test.go",
"deposits_test.go",
"effective_balance_updates_test.go",
"registry_updates_test.go",
"upgrade_test.go",
"validator_test.go",
"withdrawals_test.go",

View File

@@ -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:
//
@@ -28,7 +34,63 @@ 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)
finalizedEpoch := st.FinalizedCheckpointEpoch()
eligibleForActivationQueueValidators := make([]primitives.ValidatorIndex, 0)
eligibleForActivationValidators := make([]primitives.ValidatorIndex, 0)
if err := st.ReadFromEveryValidator(func(idx int, val state.ReadOnlyValidator) error {
alreadyActivated := false
if helpers.IsEligibleForActivationQueue(val, currentEpoch) {
eligibleForActivationQueueValidators = append(eligibleForActivationQueueValidators, primitives.ValidatorIndex(idx))
if currentEpoch+1 <= finalizedEpoch && val.ActivationEpoch() == params.BeaconConfig().FarFutureEpoch {
eligibleForActivationValidators = append(eligibleForActivationValidators, primitives.ValidatorIndex(idx))
alreadyActivated = true
}
}
if val.EffectiveBalance() <= ejectionBal && helpers.IsActiveValidator(val, currentEpoch) {
var err error
// exitQueueEpoch and churn arguments are not used in electra.
st, _, err = validators.InitiateValidatorExit(ctx, st, primitives.ValidatorIndex(idx), 0 /*exitQueueEpoch*/, 0 /*churn*/)
if err != nil {
return fmt.Errorf("failed to initiate validator exit at index %d: %w", idx, err)
}
}
if !alreadyActivated && helpers.IsEligibleForActivation(st, val) {
eligibleForActivationValidators = append(eligibleForActivationValidators, primitives.ValidatorIndex(idx))
}
return nil
}); err != nil {
return err
}
for _, idx := range eligibleForActivationQueueValidators {
val, err := st.ValidatorAtIndex(idx)
if err != nil {
return fmt.Errorf("failed to get validator at index %d: %w", idx, err)
}
val.ActivationEligibilityEpoch = currentEpoch + 1
if err := st.UpdateValidatorAtIndex(idx, val); err != nil {
return fmt.Errorf("failed to update eligible validator at index %d: %w", idx, err)
}
}
for _, idx := range eligibleForActivationValidators {
val, err := st.ValidatorAtIndex(idx)
if err != nil {
return fmt.Errorf("failed to get validator at index %d: %w", idx, err)
}
val.ActivationEpoch = activationEpoch
if err := st.UpdateValidatorAtIndex(idx, val); err != nil {
return fmt.Errorf("failed to activate validator at index %d: %w", idx, err)
}
}
return nil
}

View File

@@ -0,0 +1,148 @@
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"
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) {
const electraEpoch = 3
cfg := params.BeaconConfig()
cfg.ElectraForkEpoch = electraEpoch
params.SetActiveTestCleanup(t, cfg)
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: 6, Root: make([]byte, fieldparams.RootLength)},
}
for i := uint64(0); i < 10; i++ {
base.Validators = append(base.Validators, &eth.Validator{
ActivationEligibilityEpoch: params.BeaconConfig().FarFutureEpoch,
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: 6, 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)
}
}
}

View File

@@ -74,8 +74,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")
}

View File

@@ -41,8 +41,8 @@ var (
// Check if ``validator`` is active.
// """
// return validator.activation_epoch <= epoch < validator.exit_epoch
func IsActiveValidator(validator *ethpb.Validator, epoch primitives.Epoch) bool {
return checkValidatorActiveStatus(validator.ActivationEpoch, validator.ExitEpoch, epoch)
func IsActiveValidator(validator state.ReadOnlyValidator, epoch primitives.Epoch) bool {
return checkValidatorActiveStatus(validator.ActivationEpoch(), validator.ExitEpoch(), epoch)
}
// IsActiveValidatorUsingTrie checks if a read only validator is active.
@@ -404,11 +404,11 @@ func ComputeProposerIndex(bState state.ReadOnlyValidators, activeIndices []primi
// validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH
// and validator.effective_balance >= MIN_ACTIVATION_BALANCE # [Modified in Electra:EIP7251]
// )
func IsEligibleForActivationQueue(validator *ethpb.Validator, currentEpoch primitives.Epoch) bool {
func IsEligibleForActivationQueue(validator state.ReadOnlyValidator, currentEpoch primitives.Epoch) bool {
if currentEpoch >= params.BeaconConfig().ElectraForkEpoch {
return isEligibleForActivationQueueElectra(validator.ActivationEligibilityEpoch, validator.EffectiveBalance)
return isEligibleForActivationQueueElectra(validator.ActivationEligibilityEpoch(), validator.EffectiveBalance())
}
return isEligibleForActivationQueue(validator.ActivationEligibilityEpoch, validator.EffectiveBalance)
return isEligibleForActivationQueue(validator.ActivationEligibilityEpoch(), validator.EffectiveBalance())
}
// isEligibleForActivationQueue carries out the logic for IsEligibleForActivationQueue
@@ -459,9 +459,9 @@ func isEligibleForActivationQueueElectra(activationEligibilityEpoch primitives.E
// # Has not yet been activated
// and validator.activation_epoch == FAR_FUTURE_EPOCH
// )
func IsEligibleForActivation(state state.ReadOnlyCheckpoint, validator *ethpb.Validator) bool {
func IsEligibleForActivation(state state.ReadOnlyCheckpoint, validator state.ReadOnlyValidator) bool {
finalizedEpoch := state.FinalizedCheckpointEpoch()
return isEligibleForActivation(validator.ActivationEligibilityEpoch, validator.ActivationEpoch, finalizedEpoch)
return isEligibleForActivation(validator.ActivationEligibilityEpoch(), validator.ActivationEpoch(), finalizedEpoch)
}
// IsEligibleForActivationUsingTrie checks if the validator is eligible for activation.

View File

@@ -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",

View File

@@ -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")
}

View File

@@ -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",

View File

@@ -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")
}

View File

@@ -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",

View File

@@ -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)
}