mirror of
https://gitlab.com/pulsechaincom/prysm-pulse.git
synced 2024-12-29 06:37:17 +00:00
d58d3f2c57
* rewrite/refactor deposit testing code keep track of sent deposits so that they can be compared in detail with the validator set retreived from the API. * fix bugs in evaluator and retry * lint + deepsource appeasement * typo s/Sprintf/Printf/ * gosec, more like nosec * fix gosec number - 204->304 * type switch to get signed block from container * improve comments * centralizing constants and adding comments * lock around Depositor to avoid future races Co-authored-by: Kasey Kirkham <kasey@users.noreply.github.com>
454 lines
15 KiB
Go
454 lines
15 KiB
Go
package evaluators
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/pkg/errors"
|
|
corehelpers "github.com/prysmaticlabs/prysm/v3/beacon-chain/core/helpers"
|
|
"github.com/prysmaticlabs/prysm/v3/beacon-chain/core/signing"
|
|
"github.com/prysmaticlabs/prysm/v3/config/params"
|
|
"github.com/prysmaticlabs/prysm/v3/consensus-types/blocks"
|
|
types "github.com/prysmaticlabs/prysm/v3/consensus-types/primitives"
|
|
"github.com/prysmaticlabs/prysm/v3/encoding/bytesutil"
|
|
ethpb "github.com/prysmaticlabs/prysm/v3/proto/prysm/v1alpha1"
|
|
"github.com/prysmaticlabs/prysm/v3/testing/endtoend/helpers"
|
|
e2e "github.com/prysmaticlabs/prysm/v3/testing/endtoend/params"
|
|
"github.com/prysmaticlabs/prysm/v3/testing/endtoend/policies"
|
|
e2etypes "github.com/prysmaticlabs/prysm/v3/testing/endtoend/types"
|
|
"github.com/prysmaticlabs/prysm/v3/testing/util"
|
|
"golang.org/x/exp/rand"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/protobuf/types/known/emptypb"
|
|
)
|
|
|
|
var exitedVals = make(map[[48]byte]bool)
|
|
|
|
// churnLimit is normally 4 unless the validator set is extremely large.
|
|
var churnLimit = 4
|
|
var depositValCount = e2e.DepositCount
|
|
|
|
// Deposits should be processed in twice the length of the epochs per eth1 voting period.
|
|
var depositsInBlockStart = types.Epoch(math.Floor(float64(params.E2ETestConfig().EpochsPerEth1VotingPeriod) * 2))
|
|
|
|
// deposits included + finalization + MaxSeedLookahead for activation.
|
|
var depositActivationStartEpoch = depositsInBlockStart + 2 + params.E2ETestConfig().MaxSeedLookahead
|
|
var depositEndEpoch = depositActivationStartEpoch + types.Epoch(math.Ceil(float64(depositValCount)/float64(churnLimit)))
|
|
|
|
// 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(7),
|
|
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,
|
|
}
|
|
|
|
// 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 := ðpb.ListBlocksRequest{QueryFilter: ðpb.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()
|
|
slot := b.Slot()
|
|
eth1Data := b.Body().Eth1Data()
|
|
deposits := b.Body().Deposits()
|
|
fmt.Printf(
|
|
"Slot: %d with %d deposits, Eth1 block %#x with %d deposits\n",
|
|
slot,
|
|
len(deposits),
|
|
eth1Data.BlockHash, eth1Data.DepositCount,
|
|
)
|
|
for _, d := range deposits {
|
|
k := bytesutil.ToBytes48(d.Data.PublicKey)
|
|
v := observed[k]
|
|
observed[k] = v + d.Data.Amount
|
|
}
|
|
}
|
|
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")
|
|
}
|
|
req := ðpb.ListBlocksRequest{QueryFilter: ðpb.ListBlocksRequest_Epoch{Epoch: chainHead.HeadEpoch.Sub(1)}}
|
|
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)
|
|
if v.ActivationEpoch != epoch {
|
|
continue
|
|
}
|
|
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 proecssed, resulting in a validator.
|
|
if len(expected) > 0 {
|
|
return fmt.Errorf("missing %d validators for post-genesis deposits", len(expected))
|
|
}
|
|
|
|
if deposits != churnLimit {
|
|
return fmt.Errorf("expected %d deposits to be processed in epoch %d, received %d", churnLimit, 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 := ðpb.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
|
|
log.WithField("len", len(vals)).WithField("pageToken", pageToken).Info("getAllValidators")
|
|
}
|
|
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 := exitedVals[key]
|
|
if exited {
|
|
nexits++
|
|
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(_ e2etypes.EvaluationContext, conns ...*grpc.ClientConn) error {
|
|
conn := conns[0]
|
|
valClient := ethpb.NewBeaconNodeValidatorClient(conn)
|
|
beaconClient := ethpb.NewBeaconChainClient(conn)
|
|
|
|
ctx := context.Background()
|
|
chainHead, err := beaconClient.GetChainHead(ctx, &emptypb.Empty{})
|
|
if err != nil {
|
|
return errors.Wrap(err, "could not get chain head")
|
|
}
|
|
|
|
deposits, privKeys, err := util.DeterministicDepositsAndKeys(params.BeaconConfig().MinGenesisActiveValidatorCount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
exitedIndex := types.ValidatorIndex(rand.Uint64() % params.BeaconConfig().MinGenesisActiveValidatorCount)
|
|
|
|
voluntaryExit := ðpb.VoluntaryExit{
|
|
Epoch: chainHead.HeadEpoch,
|
|
ValidatorIndex: exitedIndex,
|
|
}
|
|
req := ðpb.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 := ðpb.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)
|
|
exitedVals[pubk] = true
|
|
|
|
return nil
|
|
}
|
|
|
|
func validatorsHaveExited(_ e2etypes.EvaluationContext, conns ...*grpc.ClientConn) error {
|
|
conn := conns[0]
|
|
client := ethpb.NewBeaconChainClient(conn)
|
|
for k := range exitedVals {
|
|
validatorRequest := ðpb.GetValidatorRequest{
|
|
QueryFilter: ðpb.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(_ 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")
|
|
}
|
|
|
|
req := ðpb.ListBlocksRequest{QueryFilter: ðpb.ListBlocksRequest_Epoch{Epoch: chainHead.HeadEpoch.Sub(1)}}
|
|
blks, err := client.ListBeaconBlocks(context.Background(), req)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to get blocks from beacon-chain")
|
|
}
|
|
|
|
for _, blk := range blks.BlockContainers {
|
|
var slot types.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
|
|
default:
|
|
return errors.New("block neither phase0,altair or bellatrix")
|
|
}
|
|
slotsPerVotingPeriod := params.E2ETestConfig().SlotsPerEpoch.Mul(uint64(params.E2ETestConfig().EpochsPerEth1VotingPeriod))
|
|
|
|
// 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 {
|
|
expectedEth1DataVote = vote
|
|
return nil
|
|
}
|
|
|
|
if !bytes.Equal(vote, expectedEth1DataVote) {
|
|
return fmt.Errorf("incorrect eth1data vote for slot %d; expected: %#x vs voted: %#x",
|
|
slot, expectedEth1DataVote, vote)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var expectedEth1DataVote []byte
|