package evaluators

import (
	"bytes"
	"context"
	"fmt"
	"math"
	"strings"

	"github.com/OffchainLabs/prysm/v6/api/client/beacon"
	"github.com/OffchainLabs/prysm/v6/api/server/structs"
	corehelpers "github.com/OffchainLabs/prysm/v6/beacon-chain/core/helpers"
	"github.com/OffchainLabs/prysm/v6/beacon-chain/core/signing"
	"github.com/OffchainLabs/prysm/v6/beacon-chain/state"
	"github.com/OffchainLabs/prysm/v6/config/params"
	"github.com/OffchainLabs/prysm/v6/consensus-types/blocks"
	"github.com/OffchainLabs/prysm/v6/consensus-types/primitives"
	"github.com/OffchainLabs/prysm/v6/encoding/bytesutil"
	"github.com/OffchainLabs/prysm/v6/encoding/ssz/detect"
	ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1"
	"github.com/OffchainLabs/prysm/v6/testing/endtoend/helpers"
	e2e "github.com/OffchainLabs/prysm/v6/testing/endtoend/params"
	"github.com/OffchainLabs/prysm/v6/testing/endtoend/policies"
	e2etypes "github.com/OffchainLabs/prysm/v6/testing/endtoend/types"
	"github.com/OffchainLabs/prysm/v6/testing/util"
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/pkg/errors"
	"golang.org/x/exp/rand"
	"google.golang.org/grpc"
	"google.golang.org/protobuf/types/known/emptypb"
)

var depositValCount = e2e.DepositCount
var numOfExits = 2

// Deposits should be processed in twice the length of the epochs per eth1 voting period.
var depositsInBlockStart = params.E2ETestConfig().EpochsPerEth1VotingPeriod * 2

// deposits included + finalization + MaxSeedLookahead for activation.
var depositActivationStartEpoch = depositsInBlockStart + 2 + params.E2ETestConfig().MaxSeedLookahead
var depositEndEpoch = depositActivationStartEpoch + primitives.Epoch(math.Ceil(float64(depositValCount)/float64(params.E2ETestConfig().MinPerEpochChurnLimit)))
var exitSubmissionEpoch = primitives.Epoch(7)

// ProcessesDepositsInBlocks ensures the expected amount of deposits are accepted into blocks.
var ProcessesDepositsInBlocks = e2etypes.Evaluator{
	Name:       "processes_deposits_in_blocks_epoch_%d",
	Policy:     policies.OnEpoch(depositsInBlockStart), // We expect all deposits to enter in one epoch.
	Evaluation: processesDepositsInBlocks,
}

// VerifyBlockGraffiti ensures the block graffiti is one of the random list.
var VerifyBlockGraffiti = e2etypes.Evaluator{
	Name:       "verify_graffiti_in_blocks_epoch_%d",
	Policy:     policies.AfterNthEpoch(0),
	Evaluation: verifyGraffitiInBlocks,
}

// ActivatesDepositedValidators ensures the expected amount of validator deposits are activated into the state.
var ActivatesDepositedValidators = e2etypes.Evaluator{
	Name:       "processes_deposit_validators_epoch_%d",
	Policy:     policies.BetweenEpochs(depositActivationStartEpoch, depositEndEpoch),
	Evaluation: activatesDepositedValidators,
}

// DepositedValidatorsAreActive ensures the expected amount of validators are active after their deposits are processed.
var DepositedValidatorsAreActive = e2etypes.Evaluator{
	Name:       "deposited_validators_are_active_epoch_%d",
	Policy:     policies.AfterNthEpoch(depositEndEpoch),
	Evaluation: depositedValidatorsAreActive,
}

// ProposeVoluntaryExit sends a voluntary exit from randomly selected validator in the genesis set.
var ProposeVoluntaryExit = e2etypes.Evaluator{
	Name:       "propose_voluntary_exit_epoch_%d",
	Policy:     policies.OnEpoch(exitSubmissionEpoch),
	Evaluation: proposeVoluntaryExit,
}

// ValidatorsHaveExited checks the beacon state for the exited validator and ensures its marked as exited.
var ValidatorsHaveExited = e2etypes.Evaluator{
	Name:       "voluntary_has_exited_%d",
	Policy:     policies.OnEpoch(8),
	Evaluation: validatorsHaveExited,
}

// SubmitWithdrawal sends a withdrawal from a previously exited validator.
var SubmitWithdrawal = e2etypes.Evaluator{
	Name: "submit_withdrawal_epoch_%d",
	Policy: func(currentEpoch primitives.Epoch) bool {
		fEpoch := params.BeaconConfig().CapellaForkEpoch
		return policies.BetweenEpochs(fEpoch-2, fEpoch+1)(currentEpoch)
	},
	Evaluation: submitWithdrawal,
}

// ValidatorsHaveWithdrawn checks the beacon state for the withdrawn validator and ensures it has been withdrawn.
var ValidatorsHaveWithdrawn = e2etypes.Evaluator{
	Name: "validator_has_withdrawn_%d",
	Policy: func(currentEpoch primitives.Epoch) bool {
		// TODO: Fix this for mainnet configs.
		if params.BeaconConfig().ConfigName != params.EndToEndName {
			return false
		}
		// Only run this for minimal setups after capella
		validWithdrawnEpoch := params.BeaconConfig().CapellaForkEpoch + 1

		requiredPolicy := policies.OnEpoch(validWithdrawnEpoch)
		return requiredPolicy(currentEpoch)
	},
	Evaluation: validatorsAreWithdrawn,
}

// ValidatorsVoteWithTheMajority verifies whether validator vote for eth1data using the majority algorithm.
var ValidatorsVoteWithTheMajority = e2etypes.Evaluator{
	Name:       "validators_vote_with_the_majority_%d",
	Policy:     policies.AfterNthEpoch(0),
	Evaluation: validatorsVoteWithTheMajority,
}

type mismatch struct {
	k [48]byte
	e uint64
	o uint64
}

func (m mismatch) String() string {
	return fmt.Sprintf("(%#x:%d:%d)", m.k, m.e, m.o)
}

func processesDepositsInBlocks(ec *e2etypes.EvaluationContext, conns ...*grpc.ClientConn) error {
	expected := ec.Balances(e2etypes.PostGenesisDepositBatch)
	conn := conns[0]
	client := ethpb.NewBeaconChainClient(conn)
	chainHead, err := client.GetChainHead(context.Background(), &emptypb.Empty{})
	if err != nil {
		return errors.Wrap(err, "failed to get chain head")
	}

	req := &ethpb.ListBlocksRequest{QueryFilter: &ethpb.ListBlocksRequest_Epoch{Epoch: chainHead.HeadEpoch - 1}}
	blks, err := client.ListBeaconBlocks(context.Background(), req)
	if err != nil {
		return errors.Wrap(err, "failed to get blocks from beacon-chain")
	}
	observed := make(map[[48]byte]uint64)
	for _, blk := range blks.BlockContainers {
		sb, err := blocks.BeaconBlockContainerToSignedBeaconBlock(blk)
		if err != nil {
			return errors.Wrap(err, "failed to convert api response type to SignedBeaconBlock interface")
		}
		b := sb.Block()
		deposits := b.Body().Deposits()
		for _, d := range deposits {
			k := bytesutil.ToBytes48(d.Data.PublicKey)
			v := observed[k]
			observed[k] = v + d.Data.Amount
		}
	}
	var mismatches []string
	for k, ev := range expected {
		ov := observed[k]
		if ev != ov {
			mismatches = append(mismatches, mismatch{k: k, e: ev, o: ov}.String())
		}
	}
	if len(mismatches) != 0 {
		return fmt.Errorf("not all expected deposits observed on chain, len(expected)=%d, len(observed)=%d, mismatches=%d; details(key:expected:observed): %s", len(expected), len(observed), len(mismatches), strings.Join(mismatches, ","))
	}
	return nil
}

func verifyGraffitiInBlocks(_ *e2etypes.EvaluationContext, conns ...*grpc.ClientConn) error {
	conn := conns[0]
	client := ethpb.NewBeaconChainClient(conn)
	chainHead, err := client.GetChainHead(context.Background(), &emptypb.Empty{})
	if err != nil {
		return errors.Wrap(err, "failed to get chain head")
	}
	begin := chainHead.HeadEpoch
	// Prevent underflow when this runs at epoch 0.
	if begin > 0 {
		begin = begin.Sub(1)
	}
	req := &ethpb.ListBlocksRequest{QueryFilter: &ethpb.ListBlocksRequest_Epoch{Epoch: begin}}
	blks, err := client.ListBeaconBlocks(context.Background(), req)
	if err != nil {
		return errors.Wrap(err, "failed to get blocks from beacon-chain")
	}
	for _, ctr := range blks.BlockContainers {
		blk, err := blocks.BeaconBlockContainerToSignedBeaconBlock(ctr)
		if err != nil {
			return err
		}
		var e bool
		slot := blk.Block().Slot()
		graffitiInBlock := blk.Block().Body().Graffiti()
		for _, graffiti := range helpers.Graffiti {
			if bytes.Equal(bytesutil.PadTo([]byte(graffiti), 32), graffitiInBlock[:]) {
				e = true
				break
			}
		}
		if !e && slot != 0 {
			return errors.New("could not get graffiti from the list")
		}
	}

	return nil
}

func activatesDepositedValidators(ec *e2etypes.EvaluationContext, conns ...*grpc.ClientConn) error {
	conn := conns[0]
	client := ethpb.NewBeaconChainClient(conn)

	chainHead, err := client.GetChainHead(context.Background(), &emptypb.Empty{})
	if err != nil {
		return errors.Wrap(err, "failed to get chain head")
	}
	epoch := chainHead.HeadEpoch

	validators, err := getAllValidators(client)
	if err != nil {
		return errors.Wrap(err, "failed to get validators")
	}
	expected := ec.Balances(e2etypes.PostGenesisDepositBatch)

	var deposits, lowBalance, wrongExit, wrongWithdraw int
	for _, v := range validators {
		key := bytesutil.ToBytes48(v.PublicKey)
		if _, ok := expected[key]; !ok {
			continue
		}
		delete(expected, key)
		// Validator can't be activated yet .
		if v.ActivationEligibilityEpoch > chainHead.FinalizedEpoch {
			continue
		}
		if v.ActivationEpoch < epoch {
			continue
		}
		if v.ActivationEpoch == epoch {
			deposits++
		}
		if v.EffectiveBalance < params.BeaconConfig().MaxEffectiveBalance {
			lowBalance++
		}
		if v.ExitEpoch != params.BeaconConfig().FarFutureEpoch {
			wrongExit++
		}
		if v.WithdrawableEpoch != params.BeaconConfig().FarFutureEpoch {
			wrongWithdraw++
		}
	}

	// Make sure every post-genesis deposit has been processed, resulting in a validator.
	if len(expected) > 0 {
		return fmt.Errorf("missing %d validators for post-genesis deposits", len(expected))
	}

	if deposits > 0 && uint64(deposits) != params.BeaconConfig().MinPerEpochChurnLimit {
		return fmt.Errorf("expected %d deposits to be processed in epoch %d, received %d", params.BeaconConfig().MinPerEpochChurnLimit, epoch, deposits)
	}

	if lowBalance > 0 {
		return fmt.Errorf(
			"%d validators did not have genesis validator effective balance of %d",
			lowBalance,
			params.BeaconConfig().MaxEffectiveBalance,
		)
	} else if wrongExit > 0 {
		return fmt.Errorf("%d validators did not have an exit epoch of far future epoch", wrongExit)
	} else if wrongWithdraw > 0 {
		return fmt.Errorf("%d validators did not have a withdrawable epoch of far future epoch", wrongWithdraw)
	}
	return nil
}

func getAllValidators(c ethpb.BeaconChainClient) ([]*ethpb.Validator, error) {
	vals := make([]*ethpb.Validator, 0)
	pageToken := "0"
	for pageToken != "" {
		validatorRequest := &ethpb.ListValidatorsRequest{
			PageSize:  100,
			PageToken: pageToken,
		}
		validators, err := c.ListValidators(context.Background(), validatorRequest)
		if err != nil {
			return nil, errors.Wrap(err, "failed to get validators")
		}
		for _, v := range validators.ValidatorList {
			vals = append(vals, v.Validator)
		}
		pageToken = validators.NextPageToken
	}
	return vals, nil
}

func depositedValidatorsAreActive(ec *e2etypes.EvaluationContext, conns ...*grpc.ClientConn) error {
	conn := conns[0]
	client := ethpb.NewBeaconChainClient(conn)

	chainHead, err := client.GetChainHead(context.Background(), &emptypb.Empty{})
	if err != nil {
		return errors.Wrap(err, "failed to get chain head")
	}

	vals, err := getAllValidators(client)
	if err != nil {
		return errors.Wrap(err, "error retrieving validator list from API")
	}
	inactive := 0
	lowBalance := 0
	nexits := 0
	expected := ec.Balances(e2etypes.PostGenesisDepositBatch)
	nexpected := len(expected)
	for _, v := range vals {
		key := bytesutil.ToBytes48(v.PublicKey)
		if _, ok := expected[key]; !ok {
			continue // we aren't checking for this validator
		}
		// ignore voluntary exits when checking balance and active status
		exited := ec.ExitedVals[key]
		if exited {
			nexits++
			delete(expected, key)
			continue
		}
		// This is to handle the changed validator activation procedure post-electra.
		if v.ActivationEligibilityEpoch != math.MaxUint64 && v.ActivationEligibilityEpoch > chainHead.FinalizedEpoch {
			delete(expected, key)
			continue
		}
		if v.ActivationEpoch != math.MaxUint64 && v.ActivationEpoch > chainHead.HeadEpoch {
			delete(expected, key)
			continue
		}
		if !corehelpers.IsActiveValidator(v, chainHead.HeadEpoch) {
			inactive++
		}
		if v.EffectiveBalance < params.BeaconConfig().MaxEffectiveBalance {
			lowBalance++
		}
		delete(expected, key)
	}
	if len(expected) > 0 {
		mk := make([]string, 0)
		for k := range expected {
			mk = append(mk, fmt.Sprintf("%#x", k))
		}
		return fmt.Errorf("API response missing %d validators, based on deposits; keys=%s", len(expected), strings.Join(mk, ","))
	}
	if inactive != 0 || lowBalance != 0 {
		return fmt.Errorf("active validator set does not match %d total deposited. %d exited, %d inactive, %d low balance", nexpected, nexits, inactive, lowBalance)
	}

	return nil
}

func proposeVoluntaryExit(ec *e2etypes.EvaluationContext, conns ...*grpc.ClientConn) error {
	conn := conns[0]
	valClient := ethpb.NewBeaconNodeValidatorClient(conn)
	beaconClient := ethpb.NewBeaconChainClient(conn)
	debugClient := ethpb.NewDebugClient(conn)

	ctx := context.Background()
	chainHead, err := beaconClient.GetChainHead(ctx, &emptypb.Empty{})
	if err != nil {
		return errors.Wrap(err, "could not get chain head")
	}
	stObj, err := debugClient.GetBeaconState(ctx, &ethpb.BeaconStateRequest{QueryFilter: &ethpb.BeaconStateRequest_Slot{Slot: chainHead.HeadSlot}})
	if err != nil {
		return errors.Wrap(err, "could not get state object")
	}
	versionedMarshaler, err := detect.FromState(stObj.Encoded)
	if err != nil {
		return errors.Wrap(err, "could not get state marshaler")
	}
	st, err := versionedMarshaler.UnmarshalBeaconState(stObj.Encoded)
	if err != nil {
		return errors.Wrap(err, "could not get state")
	}
	var execIndices []int
	err = st.ReadFromEveryValidator(func(idx int, val state.ReadOnlyValidator) error {
		if val.GetWithdrawalCredentials()[0] == params.BeaconConfig().ETH1AddressWithdrawalPrefixByte {
			execIndices = append(execIndices, idx)
		}
		return nil
	})
	if err != nil {
		return err
	}
	if len(execIndices) > numOfExits {
		execIndices = execIndices[:numOfExits]
	}

	deposits, privKeys, err := util.DeterministicDepositsAndKeys(params.BeaconConfig().MinGenesisActiveValidatorCount)
	if err != nil {
		return err
	}

	var sendExit = func(exitedIndex primitives.ValidatorIndex) error {
		voluntaryExit := &ethpb.VoluntaryExit{
			Epoch:          chainHead.HeadEpoch,
			ValidatorIndex: exitedIndex,
		}
		req := &ethpb.DomainRequest{
			Epoch:  chainHead.HeadEpoch,
			Domain: params.BeaconConfig().DomainVoluntaryExit[:],
		}
		domain, err := valClient.DomainData(ctx, req)
		if err != nil {
			return err
		}
		signingData, err := signing.ComputeSigningRoot(voluntaryExit, domain.SignatureDomain)
		if err != nil {
			return err
		}
		signature := privKeys[exitedIndex].Sign(signingData[:])
		signedExit := &ethpb.SignedVoluntaryExit{
			Exit:      voluntaryExit,
			Signature: signature.Marshal(),
		}

		if _, err = valClient.ProposeExit(ctx, signedExit); err != nil {
			return errors.Wrap(err, "could not propose exit")
		}
		pubk := bytesutil.ToBytes48(deposits[exitedIndex].Data.PublicKey)
		ec.ExitedVals[pubk] = true
		return nil
	}

	// Send exits for keys which already contain execution credentials.
	for _, idx := range execIndices {
		if err := sendExit(primitives.ValidatorIndex(idx)); err != nil {
			return err
		}
	}

	// Send an exit for a non-exited validator.
	for i := 0; i < numOfExits; {
		randIndex := primitives.ValidatorIndex(rand.Uint64() % params.BeaconConfig().MinGenesisActiveValidatorCount)
		if ec.ExitedVals[bytesutil.ToBytes48(privKeys[randIndex].PublicKey().Marshal())] {
			continue
		}
		if err := sendExit(randIndex); err != nil {
			return err
		}
		i++
	}

	return nil
}

func validatorsHaveExited(ec *e2etypes.EvaluationContext, conns ...*grpc.ClientConn) error {
	conn := conns[0]
	client := ethpb.NewBeaconChainClient(conn)
	for k := range ec.ExitedVals {
		validatorRequest := &ethpb.GetValidatorRequest{
			QueryFilter: &ethpb.GetValidatorRequest_PublicKey{
				PublicKey: k[:],
			},
		}
		validator, err := client.GetValidator(context.Background(), validatorRequest)
		if err != nil {
			return errors.Wrap(err, "failed to get validators")
		}
		if validator.ExitEpoch == params.BeaconConfig().FarFutureEpoch {
			return fmt.Errorf("expected validator %#x to be submitted for exit", k)
		}
	}
	return nil
}

func validatorsVoteWithTheMajority(ec *e2etypes.EvaluationContext, conns ...*grpc.ClientConn) error {
	conn := conns[0]
	client := ethpb.NewBeaconChainClient(conn)
	chainHead, err := client.GetChainHead(context.Background(), &emptypb.Empty{})
	if err != nil {
		return errors.Wrap(err, "failed to get chain head")
	}

	begin := chainHead.HeadEpoch
	// Prevent underflow when this runs at epoch 0.
	if begin > 0 {
		begin = begin.Sub(1)
	}
	req := &ethpb.ListBlocksRequest{QueryFilter: &ethpb.ListBlocksRequest_Epoch{Epoch: begin}}
	blks, err := client.ListBeaconBlocks(context.Background(), req)
	if err != nil {
		return errors.Wrap(err, "failed to get blocks from beacon-chain")
	}

	slotsPerVotingPeriod := params.E2ETestConfig().SlotsPerEpoch.Mul(uint64(params.E2ETestConfig().EpochsPerEth1VotingPeriod))
	for _, blk := range blks.BlockContainers {
		var slot primitives.Slot
		var vote []byte
		switch blk.Block.(type) {
		case *ethpb.BeaconBlockContainer_Phase0Block:
			b := blk.GetPhase0Block().Block
			slot = b.Slot
			vote = b.Body.Eth1Data.BlockHash
		case *ethpb.BeaconBlockContainer_AltairBlock:
			b := blk.GetAltairBlock().Block
			slot = b.Slot
			vote = b.Body.Eth1Data.BlockHash
		case *ethpb.BeaconBlockContainer_BellatrixBlock:
			b := blk.GetBellatrixBlock().Block
			slot = b.Slot
			vote = b.Body.Eth1Data.BlockHash
		case *ethpb.BeaconBlockContainer_BlindedBellatrixBlock:
			b := blk.GetBlindedBellatrixBlock().Block
			slot = b.Slot
			vote = b.Body.Eth1Data.BlockHash
		case *ethpb.BeaconBlockContainer_CapellaBlock:
			b := blk.GetCapellaBlock().Block
			slot = b.Slot
			vote = b.Body.Eth1Data.BlockHash
		case *ethpb.BeaconBlockContainer_BlindedCapellaBlock:
			b := blk.GetBlindedCapellaBlock().Block
			slot = b.Slot
			vote = b.Body.Eth1Data.BlockHash
		case *ethpb.BeaconBlockContainer_DenebBlock:
			b := blk.GetDenebBlock().Block
			slot = b.Slot
			vote = b.Body.Eth1Data.BlockHash
		case *ethpb.BeaconBlockContainer_BlindedDenebBlock:
			b := blk.GetBlindedDenebBlock().Message
			slot = b.Slot
			vote = b.Body.Eth1Data.BlockHash
		case *ethpb.BeaconBlockContainer_ElectraBlock:
			b := blk.GetElectraBlock().Block
			slot = b.Slot
			vote = b.Body.Eth1Data.BlockHash
		case *ethpb.BeaconBlockContainer_BlindedElectraBlock:
			b := blk.GetBlindedElectraBlock().Message
			slot = b.Slot
			vote = b.Body.Eth1Data.BlockHash
		default:
			return fmt.Errorf("block of type %T is unknown", blk.Block)
		}
		ec.SeenVotes[slot] = vote

		// We treat epoch 1 differently from other epoch for two reasons:
		// - this evaluator is not executed for epoch 0 so we have to calculate the first slot differently
		// - for some reason the vote for the first slot in epoch 1 is 0x000... so we skip this slot
		var isFirstSlotInVotingPeriod bool
		if chainHead.HeadEpoch == 1 && slot%params.BeaconConfig().SlotsPerEpoch == 0 {
			continue
		}
		// We skipped the first slot so we treat the second slot as the starting slot of epoch 1.
		if chainHead.HeadEpoch == 1 {
			isFirstSlotInVotingPeriod = slot%params.BeaconConfig().SlotsPerEpoch == 1
		} else {
			isFirstSlotInVotingPeriod = slot%slotsPerVotingPeriod == 0
		}
		if isFirstSlotInVotingPeriod {
			ec.ExpectedEth1DataVote = vote
			return nil
		}

		if !bytes.Equal(vote, ec.ExpectedEth1DataVote) {
			for i := primitives.Slot(0); i < slot; i++ {
				v, ok := ec.SeenVotes[i]
				if ok {
					fmt.Printf("vote at slot=%d = %#x\n", i, v)
				} else {
					fmt.Printf("did not see slot=%d\n", i)
				}
			}
			return fmt.Errorf("incorrect eth1data vote for slot %d; expected: %#x vs voted: %#x",
				slot, ec.ExpectedEth1DataVote, vote)
		}
	}
	return nil
}

func submitWithdrawal(ec *e2etypes.EvaluationContext, conns ...*grpc.ClientConn) error {
	conn := conns[0]
	beaconClient := ethpb.NewBeaconChainClient(conn)
	debugClient := ethpb.NewDebugClient(conn)

	ctx := context.Background()
	chainHead, err := beaconClient.GetChainHead(ctx, &emptypb.Empty{})
	if err != nil {
		return errors.Wrap(err, "could not get chain head")
	}
	stObj, err := debugClient.GetBeaconState(ctx, &ethpb.BeaconStateRequest{QueryFilter: &ethpb.BeaconStateRequest_Slot{Slot: chainHead.HeadSlot}})
	if err != nil {
		return errors.Wrap(err, "could not get state object")
	}
	versionedMarshaler, err := detect.FromState(stObj.Encoded)
	if err != nil {
		return errors.Wrap(err, "could not get state marshaler")
	}
	st, err := versionedMarshaler.UnmarshalBeaconState(stObj.Encoded)
	if err != nil {
		return errors.Wrap(err, "could not get state")
	}
	exitedIndices := make([]primitives.ValidatorIndex, 0)

	for key := range ec.ExitedVals {
		valIdx, ok := st.ValidatorIndexByPubkey(key)
		if !ok {
			return errors.Errorf("pubkey %#x does not exist in our state", key)
		}
		exitedIndices = append(exitedIndices, valIdx)
	}

	_, privKeys, err := util.DeterministicDepositsAndKeys(params.BeaconConfig().MinGenesisActiveValidatorCount)
	if err != nil {
		return err
	}
	changes := make([]*structs.SignedBLSToExecutionChange, 0)
	// Only send half the number of changes each time, to allow us to test
	// at the fork boundary.
	wantedChanges := numOfExits / 2
	for _, idx := range exitedIndices {
		// Exit sending more change messages.
		if len(changes) >= wantedChanges {
			break
		}
		val, err := st.ValidatorAtIndex(idx)
		if err != nil {
			return err
		}
		if val.WithdrawalCredentials[0] == params.BeaconConfig().ETH1AddressWithdrawalPrefixByte {
			continue
		}
		if !bytes.Equal(val.PublicKey, privKeys[idx].PublicKey().Marshal()) {
			return errors.Errorf("pubkey is not equal, wanted %#x but received %#x", val.PublicKey, privKeys[idx].PublicKey().Marshal())
		}
		message := &ethpb.BLSToExecutionChange{
			ValidatorIndex:     idx,
			FromBlsPubkey:      privKeys[idx].PublicKey().Marshal(),
			ToExecutionAddress: bytesutil.ToBytes(uint64(idx), 20),
		}
		domain, err := signing.ComputeDomain(params.BeaconConfig().DomainBLSToExecutionChange, params.BeaconConfig().GenesisForkVersion, st.GenesisValidatorsRoot())
		if err != nil {
			return err
		}
		sigRoot, err := signing.ComputeSigningRoot(message, domain)
		if err != nil {
			return err
		}
		signature := privKeys[idx].Sign(sigRoot[:]).Marshal()

		changes = append(changes, &structs.SignedBLSToExecutionChange{
			Message:   structs.BLSChangeFromConsensus(message),
			Signature: hexutil.Encode(signature),
		})
	}

	beaconAPIClient, err := beacon.NewClient(fmt.Sprintf("http://localhost:%d/eth/v1", e2e.TestParams.Ports.PrysmBeaconNodeHTTPPort)) // only uses the first node so no updates to port
	if err != nil {
		return err
	}

	return beaconAPIClient.SubmitChangeBLStoExecution(ctx, changes)
}

func validatorsAreWithdrawn(ec *e2etypes.EvaluationContext, conns ...*grpc.ClientConn) error {
	conn := conns[0]
	beaconClient := ethpb.NewBeaconChainClient(conn)
	debugClient := ethpb.NewDebugClient(conn)

	ctx := context.Background()
	chainHead, err := beaconClient.GetChainHead(ctx, &emptypb.Empty{})
	if err != nil {
		return errors.Wrap(err, "could not get chain head")
	}
	stObj, err := debugClient.GetBeaconState(ctx, &ethpb.BeaconStateRequest{QueryFilter: &ethpb.BeaconStateRequest_Slot{Slot: chainHead.HeadSlot}})
	if err != nil {
		return errors.Wrap(err, "could not get state object")
	}
	versionedMarshaler, err := detect.FromState(stObj.Encoded)
	if err != nil {
		return errors.Wrap(err, "could not get state marshaler")
	}
	st, err := versionedMarshaler.UnmarshalBeaconState(stObj.Encoded)
	if err != nil {
		return errors.Wrap(err, "could not get state")
	}

	for key := range ec.ExitedVals {
		valIdx, ok := st.ValidatorIndexByPubkey(key)
		if !ok {
			return errors.Errorf("pubkey %#x does not exist in our state", key)
		}
		bal, err := st.BalanceAtIndex(valIdx)
		if err != nil {
			return err
		}
		// Only return an error if the validator has more than 1 eth
		// in its balance.
		if bal > 1*params.BeaconConfig().GweiPerEth {
			return errors.Errorf("Validator index %d with key %#x hasn't withdrawn. Their balance is %d.", valIdx, key, bal)
		}
	}
	return nil
}
