diff --git a/beacon-chain/core/blocks/exit.go b/beacon-chain/core/blocks/exit.go index 046b9bbc1a49..99da22fae90a 100644 --- a/beacon-chain/core/blocks/exit.go +++ b/beacon-chain/core/blocks/exit.go @@ -98,6 +98,8 @@ func ProcessVoluntaryExits( // assert get_current_epoch(state) >= voluntary_exit.epoch // # Verify the validator has been active long enough // assert get_current_epoch(state) >= validator.activation_epoch + SHARD_COMMITTEE_PERIOD +// # Only exit validator if it has no pending withdrawals in the queue +// assert get_pending_balance_to_withdraw(state, voluntary_exit.validator_index) == 0 # [New in Electra:EIP7251] // # Verify signature // domain = get_domain(state, DOMAIN_VOLUNTARY_EXIT, voluntary_exit.epoch) // signing_root = compute_signing_root(voluntary_exit, domain) @@ -113,7 +115,6 @@ func VerifyExitAndSignature( return errors.New("nil exit") } - currentSlot := state.Slot() fork := state.Fork() genesisRoot := state.GenesisValidatorsRoot() @@ -128,7 +129,7 @@ func VerifyExitAndSignature( } exit := signed.Exit - if err := verifyExitConditions(validator, currentSlot, exit); err != nil { + if err := verifyExitConditions(state, validator, exit); err != nil { return err } domain, err := signing.Domain(fork, exit.Epoch, params.BeaconConfig().DomainVoluntaryExit, genesisRoot) @@ -157,14 +158,16 @@ func VerifyExitAndSignature( // assert get_current_epoch(state) >= voluntary_exit.epoch // # Verify the validator has been active long enough // assert get_current_epoch(state) >= validator.activation_epoch + SHARD_COMMITTEE_PERIOD +// # Only exit validator if it has no pending withdrawals in the queue +// assert get_pending_balance_to_withdraw(state, voluntary_exit.validator_index) == 0 # [New in Electra:EIP7251] // # Verify signature // domain = get_domain(state, DOMAIN_VOLUNTARY_EXIT, voluntary_exit.epoch) // signing_root = compute_signing_root(voluntary_exit, domain) // assert bls.Verify(validator.pubkey, signing_root, signed_voluntary_exit.signature) // # Initiate exit // initiate_validator_exit(state, voluntary_exit.validator_index) -func verifyExitConditions(validator state.ReadOnlyValidator, currentSlot primitives.Slot, exit *ethpb.VoluntaryExit) error { - currentEpoch := slots.ToEpoch(currentSlot) +func verifyExitConditions(st state.ReadOnlyBeaconState, validator state.ReadOnlyValidator, exit *ethpb.VoluntaryExit) error { + currentEpoch := slots.ToEpoch(st.Slot()) // Verify the validator is active. if !helpers.IsActiveValidatorUsingTrie(validator, currentEpoch) { return errors.New("non-active validator cannot exit") @@ -187,5 +190,17 @@ func verifyExitConditions(validator state.ReadOnlyValidator, currentSlot primiti validator.ActivationEpoch()+params.BeaconConfig().ShardCommitteePeriod, ) } + + if st.Version() >= version.Electra { + // Only exit validator if it has no pending withdrawals in the queue. + pbw, err := st.PendingBalanceToWithdraw(exit.ValidatorIndex) + if err != nil { + return fmt.Errorf("unable to retrieve pending balance to withdraw for validator %d: %w", exit.ValidatorIndex, err) + } + if pbw != 0 { + return fmt.Errorf("validator %d must have no pending balance to withdraw, got %d pending balance to withdraw", exit.ValidatorIndex, pbw) + } + } + return nil } diff --git a/beacon-chain/core/blocks/exit_test.go b/beacon-chain/core/blocks/exit_test.go index d40f35f7a2be..bda5f7ef81b4 100644 --- a/beacon-chain/core/blocks/exit_test.go +++ b/beacon-chain/core/blocks/exit_test.go @@ -135,6 +135,11 @@ func TestProcessVoluntaryExits_AppliesCorrectStatus(t *testing.T) { } func TestVerifyExitAndSignature(t *testing.T) { + // Remove after electra fork epoch is defined. + cfg := params.BeaconConfig() + cfg.ElectraForkEpoch = cfg.DenebForkEpoch * 2 + params.SetActiveTestCleanup(t, cfg) + // End remove section. denebSlot, err := slots.EpochStart(params.BeaconConfig().DenebForkEpoch) require.NoError(t, err) tests := []struct { @@ -167,7 +172,7 @@ func TestVerifyExitAndSignature(t *testing.T) { wantErr: "nil exit", }, { - name: "Happy Path", + name: "Happy Path phase0", setup: func() (*ethpb.Validator, *ethpb.SignedVoluntaryExit, state.ReadOnlyBeaconState, error) { fork := ðpb.Fork{ PreviousVersion: params.BeaconConfig().GenesisForkVersion, @@ -275,6 +280,52 @@ func TestVerifyExitAndSignature(t *testing.T) { return validator, signedExit, bs, nil }, }, + { + name: "EIP-7251 - pending balance to withdraw must be zero", + setup: func() (*ethpb.Validator, *ethpb.SignedVoluntaryExit, state.ReadOnlyBeaconState, error) { + fork := ðpb.Fork{ + PreviousVersion: params.BeaconConfig().DenebForkVersion, + CurrentVersion: params.BeaconConfig().ElectraForkVersion, + Epoch: params.BeaconConfig().ElectraForkEpoch, + } + signedExit := ðpb.SignedVoluntaryExit{ + Exit: ðpb.VoluntaryExit{ + Epoch: params.BeaconConfig().DenebForkEpoch + 1, + ValidatorIndex: 0, + }, + } + electraSlot, err := slots.EpochStart(params.BeaconConfig().ElectraForkEpoch) + require.NoError(t, err) + bs, keys := util.DeterministicGenesisState(t, 1) + bs, err = state_native.InitializeFromProtoUnsafeElectra(ðpb.BeaconStateElectra{ + GenesisValidatorsRoot: bs.GenesisValidatorsRoot(), + Fork: fork, + Slot: electraSlot, + Validators: bs.Validators(), + }) + if err != nil { + return nil, nil, nil, err + } + validator := bs.Validators()[0] + validator.ActivationEpoch = 1 + err = bs.UpdateValidatorAtIndex(0, validator) + require.NoError(t, err) + sb, err := signing.ComputeDomainAndSign(bs, signedExit.Exit.Epoch, signedExit.Exit, params.BeaconConfig().DomainVoluntaryExit, keys[0]) + require.NoError(t, err) + sig, err := bls.SignatureFromBytes(sb) + require.NoError(t, err) + signedExit.Signature = sig.Marshal() + + // Give validator a pending balance to withdraw. + require.NoError(t, bs.AppendPendingPartialWithdrawal(ðpb.PendingPartialWithdrawal{ + Index: 0, + Amount: 500, + })) + + return validator, signedExit, bs, nil + }, + wantErr: "validator 0 must have no pending balance to withdraw", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/testing/spectest/mainnet/electra/operations/voluntary_exit_test.go b/testing/spectest/mainnet/electra/operations/voluntary_exit_test.go index ed87d6df8b87..c0f18adecf2f 100644 --- a/testing/spectest/mainnet/electra/operations/voluntary_exit_test.go +++ b/testing/spectest/mainnet/electra/operations/voluntary_exit_test.go @@ -7,6 +7,5 @@ import ( ) func TestMainnet_Electra_Operations_VoluntaryExit(t *testing.T) { - t.Skip("TODO: Electra") operations.RunVoluntaryExitTest(t, "mainnet") } diff --git a/testing/spectest/minimal/electra/operations/voluntary_exit_test.go b/testing/spectest/minimal/electra/operations/voluntary_exit_test.go index ab3098ed6b0c..ef54818ada01 100644 --- a/testing/spectest/minimal/electra/operations/voluntary_exit_test.go +++ b/testing/spectest/minimal/electra/operations/voluntary_exit_test.go @@ -7,6 +7,5 @@ import ( ) func TestMinimal_Electra_Operations_VoluntaryExit(t *testing.T) { - t.Skip("TODO: Electra") operations.RunVoluntaryExitTest(t, "minimal") }