diff --git a/beacon-chain/chaintest/tests/state-tests/block-processing.yaml b/beacon-chain/chaintest/tests/state-tests/block-processing.yaml index 0d20bad98169..6c647819e3cd 100644 --- a/beacon-chain/chaintest/tests/state-tests/block-processing.yaml +++ b/beacon-chain/chaintest/tests/state-tests/block-processing.yaml @@ -52,14 +52,12 @@ test_cases: slashable_attestation_1_validator_indices: [1, 2, 3, 4, 5, 6, 7, 51] slashable_attestation_2_custody_bitfield: !binary "F" slashable_attestation_2_validator_indices: [1, 2, 3, 4, 5, 6, 7, 51] - validator_exits: - - epoch: 144115188075855872 - validator_index: 45 # At slot 9223372036854775868, validator at index 45 triggers a voluntary exit + # TODO(2307): Validator rotation logic is getting revamped piece by piece, + # we'll add the Exit test case back when state transition aligns spec v0.6. results: slot: 9223372036854775872 num_validators: 67 penalized_validators: [50, 51] # We test that the validators at indices were indeed penalized - exited_validators: [45] # We confirm the indices of validators that willingly exited the registry # TODO(1387): Waiting for spec to stable to proceed with this test case # - config: # skip_slots: [10, 20] diff --git a/beacon-chain/core/blocks/block_operations.go b/beacon-chain/core/blocks/block_operations.go index 7b0fdee71bf3..e086e2b2b59f 100644 --- a/beacon-chain/core/blocks/block_operations.go +++ b/beacon-chain/core/blocks/block_operations.go @@ -732,6 +732,7 @@ func ProcessValidatorExits( func verifyExit(beaconState *pb.BeaconState, exit *pb.VoluntaryExit, verifySignatures bool) error { validator := beaconState.ValidatorRegistry[exit.ValidatorIndex] currentEpoch := helpers.CurrentEpoch(beaconState) + delayedActivationExitEpoch := helpers.DelayedActivationExitEpoch(currentEpoch) if validator.ExitEpoch <= delayedActivationExitEpoch { return fmt.Errorf( diff --git a/beacon-chain/core/blocks/block_operations_test.go b/beacon-chain/core/blocks/block_operations_test.go index d8f7ba1d906e..7df6613ef6ad 100644 --- a/beacon-chain/core/blocks/block_operations_test.go +++ b/beacon-chain/core/blocks/block_operations_test.go @@ -1504,7 +1504,8 @@ func TestProcessValidatorExits_AppliesCorrectStatus(t *testing.T) { t.Fatalf("Could not process exits: %v", err) } newRegistry := newState.ValidatorRegistry - if newRegistry[0].StatusFlags == pb.Validator_INITIAL { - t.Error("Expected validator status to change, remained INITIAL") + if newRegistry[0].ExitEpoch != helpers.DelayedActivationExitEpoch(state.Slot/params.BeaconConfig().SlotsPerEpoch) { + t.Errorf("Expected validator exit epoch to be %d, got %d", + helpers.DelayedActivationExitEpoch(state.Slot/params.BeaconConfig().SlotsPerEpoch), newRegistry[0].ExitEpoch) } } diff --git a/beacon-chain/core/validators/validator.go b/beacon-chain/core/validators/validator.go index 28fd785868fe..8baa39192813 100644 --- a/beacon-chain/core/validators/validator.go +++ b/beacon-chain/core/validators/validator.go @@ -179,11 +179,63 @@ func ActivateValidator(state *pb.BeaconState, idx uint64, genesis bool) (*pb.Bea // // Spec pseudocode definition: // def initiate_validator_exit(state: BeaconState, index: ValidatorIndex) -> None: +// """ +// Initiate the validator of the given ``index``. +// Note that this function mutates ``state``. +// """ +// # Return if validator already initiated exit // validator = state.validator_registry[index] -// validator.status_flags |= INITIATED_EXIT +// if validator.exit_epoch != FAR_FUTURE_EPOCH: +// return +// +// # Compute exit queue epoch +// exit_epochs = [v.exit_epoch for v in state.validator_registry if v.exit_epoch != FAR_FUTURE_EPOCH] +// exit_queue_epoch = max(exit_epochs + [get_delayed_activation_exit_epoch(get_current_epoch(state))]) +// exit_queue_churn = len([v for v in state.validator_registry if v.exit_epoch == exit_queue_epoch]) +// if exit_queue_churn >= get_churn_limit(state): +// exit_queue_epoch += 1 +// +// # Set validator exit epoch and withdrawable epoch +// validator.exit_epoch = exit_queue_epoch +// validator.withdrawable_epoch = validator.exit_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY func InitiateValidatorExit(state *pb.BeaconState, idx uint64) *pb.BeaconState { - state.ValidatorRegistry[idx].StatusFlags |= - pb.Validator_INITIATED_EXIT + v := state.ValidatorRegistry[idx] + + // Return if validator already initiated exit. + // According to the spec, this is not an assert condition and + // shouldn't fail beacon block state transition. + if v.ExitEpoch != params.BeaconConfig().FarFutureEpoch { + return state + } + + // Find the highest exit epoch among exited validators. + highestExitEpoch := helpers.DelayedActivationExitEpoch(helpers.CurrentEpoch(state)) + for i := 0; i < len(state.ValidatorRegistry); i++ { + if state.ValidatorRegistry[i].ExitEpoch != params.BeaconConfig().FarFutureEpoch { + if highestExitEpoch < state.ValidatorRegistry[i].ExitEpoch { + highestExitEpoch = state.ValidatorRegistry[i].ExitEpoch + } + } + } + + // Find the total number of validators exiting same epoch as + // input validator. If the number is greater than churn limit, postpone + // exit epoch to the next epoch. + var currentExitQueueLength uint64 + for i := 0; i < len(state.ValidatorRegistry); i++ { + if state.ValidatorRegistry[i].ExitEpoch == highestExitEpoch { + currentExitQueueLength++ + } + } + + if currentExitQueueLength >= helpers.ChurnLimit(state) { + highestExitEpoch++ + } + + v.ExitEpoch = highestExitEpoch + v.WithdrawableEpoch = v.ExitEpoch + params.BeaconConfig().MinValidatorWithdrawalDelay + + state.ValidatorRegistry[idx] = v return state } @@ -307,7 +359,6 @@ func UpdateRegistry(state *pb.BeaconState) (*pb.BeaconState, error) { currentEpoch := helpers.CurrentEpoch(state) updatedEpoch := helpers.DelayedActivationExitEpoch(currentEpoch) activeValidatorIndices := helpers.ActiveValidatorIndices(state, currentEpoch) - totalBalance := helpers.TotalBalance(state, activeValidatorIndices) // The maximum balance churn in Gwei (for deposits and exits separately). diff --git a/beacon-chain/core/validators/validator_test.go b/beacon-chain/core/validators/validator_test.go index f115a8860f20..8a63d5bdfc6b 100644 --- a/beacon-chain/core/validators/validator_test.go +++ b/beacon-chain/core/validators/validator_test.go @@ -398,14 +398,53 @@ func TestActivateValidator_OK(t *testing.T) { } } -func TestInitiateValidatorExit_OK(t *testing.T) { - state := &pb.BeaconState{ValidatorRegistry: []*pb.Validator{{}, {}, {}}} - newState := InitiateValidatorExit(state, 2) - if newState.ValidatorRegistry[0].StatusFlags != pb.Validator_INITIAL { - t.Errorf("Wanted flag INITIAL, got %v", newState.ValidatorRegistry[0].StatusFlags) - } - if newState.ValidatorRegistry[2].StatusFlags != pb.Validator_INITIATED_EXIT { - t.Errorf("Wanted flag ACTIVE_PENDING_EXIT, got %v", newState.ValidatorRegistry[0].StatusFlags) +func TestInitiateValidatorExit_AlreadyExited(t *testing.T) { + exitEpoch := uint64(199) + state := &pb.BeaconState{ValidatorRegistry: []*pb.Validator{{ + ExitEpoch: exitEpoch}, + }} + newState := InitiateValidatorExit(state, 0) + if newState.ValidatorRegistry[0].ExitEpoch != exitEpoch { + t.Errorf("Already exited, wanted exit epoch %d, got %d", + exitEpoch, newState.ValidatorRegistry[0].ExitEpoch) + } +} + +func TestInitiateValidatorExit_ProperExit(t *testing.T) { + exitedEpoch := uint64(100) + idx := uint64(3) + state := &pb.BeaconState{ValidatorRegistry: []*pb.Validator{ + {ExitEpoch: exitedEpoch}, + {ExitEpoch: exitedEpoch + 1}, + {ExitEpoch: exitedEpoch + 2}, + {ExitEpoch: params.BeaconConfig().FarFutureEpoch}, + }} + newState := InitiateValidatorExit(state, idx) + if newState.ValidatorRegistry[idx].ExitEpoch != exitedEpoch+2 { + t.Errorf("Exit epoch was not the highest, wanted exit epoch %d, got %d", + exitedEpoch+2, newState.ValidatorRegistry[idx].ExitEpoch) + } +} + +func TestInitiateValidatorExit_ChurnOverflow(t *testing.T) { + exitedEpoch := uint64(100) + idx := uint64(4) + state := &pb.BeaconState{ValidatorRegistry: []*pb.Validator{ + {ExitEpoch: exitedEpoch + 2}, + {ExitEpoch: exitedEpoch + 2}, + {ExitEpoch: exitedEpoch + 2}, + {ExitEpoch: exitedEpoch + 2}, //over flow here + {ExitEpoch: params.BeaconConfig().FarFutureEpoch}, + }} + newState := InitiateValidatorExit(state, idx) + + // Because of exit queue overflow, + // validator who init exited has to wait one more epoch. + wantedEpoch := state.ValidatorRegistry[0].ExitEpoch + 1 + + if newState.ValidatorRegistry[idx].ExitEpoch != wantedEpoch { + t.Errorf("Exit epoch did not cover overflow case, wanted exit epoch %d, got %d", + wantedEpoch, newState.ValidatorRegistry[idx].ExitEpoch) } }