package evaluators import ( "bytes" "context" "encoding/json" "fmt" "net/http" "strconv" "strings" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" types "github.com/prysmaticlabs/eth2-types" sharedparams "github.com/prysmaticlabs/prysm/config/params" "github.com/prysmaticlabs/prysm/proto/eth/service" ethpbv1 "github.com/prysmaticlabs/prysm/proto/eth/v1" ethpbv2 "github.com/prysmaticlabs/prysm/proto/eth/v2" "github.com/prysmaticlabs/prysm/testing/endtoend/params" "github.com/prysmaticlabs/prysm/testing/endtoend/policies" e2etypes "github.com/prysmaticlabs/prysm/testing/endtoend/types" "google.golang.org/grpc" ) type validatorJson struct { PublicKey string `json:"pubkey"` WithdrawalCredentials string `json:"withdrawal_credentials"` EffectiveBalance string `json:"effective_balance"` Slashed bool `json:"slashed"` ActivationEligibilityEpoch string `json:"activation_eligibility_epoch"` ActivationEpoch string `json:"activation_epoch"` ExitEpoch string `json:"exit_epoch"` WithdrawableEpoch string `json:"withdrawable_epoch"` } type validatorContainerJson struct { Index string `json:"index"` Balance string `json:"balance"` Status string `json:"status"` Validator *validatorJson `json:"validator"` } // APIMiddlewareVerifyIntegrity tests our API Middleware for the official Ethereum API. // This ensures our API Middleware returns good data compared to gRPC. var APIMiddlewareVerifyIntegrity = e2etypes.Evaluator{ Name: "api_middleware_verify_integrity_epoch_%d", Policy: policies.OnEpoch(sharedparams.AltairE2EForkEpoch), Evaluation: apiMiddlewareVerify, } const ( v1MiddlewarePathTemplate = "http://localhost:%d/eth/v1" ) func apiMiddlewareVerify(conns ...*grpc.ClientConn) error { for beaconNodeIdx, conn := range conns { if err := runAPIComparisonFunctions( beaconNodeIdx, conn, withCompareValidatorsEth, withCompareSyncCommittee, withCompareAttesterDuties, ); err != nil { return err } } return nil } func withCompareValidatorsEth(beaconNodeIdx int, conn *grpc.ClientConn) error { type stateValidatorsResponseJson struct { Data []*validatorContainerJson `json:"data"` } ctx := context.Background() beaconClient := service.NewBeaconChainClient(conn) resp, err := beaconClient.ListValidators(ctx, ðpbv1.StateValidatorsRequest{ StateId: []byte("head"), Status: []ethpbv1.ValidatorStatus{ethpbv1.ValidatorStatus_EXITED}, }) if err != nil { return err } respJSON := &stateValidatorsResponseJson{} if err := doMiddlewareJSONGetRequestV1( "/beacon/states/head/validators?status=exited", beaconNodeIdx, respJSON, ); err != nil { return err } if len(respJSON.Data) != len(resp.Data) { return fmt.Errorf( "API Middleware number of validators %d does not match gRPC %d", len(respJSON.Data), len(resp.Data), ) } resp, err = beaconClient.ListValidators(ctx, ðpbv1.StateValidatorsRequest{ StateId: []byte("head"), Id: [][]byte{[]byte("100"), []byte("200")}, }) if err != nil { return err } if err := doMiddlewareJSONGetRequestV1( "/beacon/states/head/validators?id=100&id=200", beaconNodeIdx, respJSON, ); err != nil { return err } if len(respJSON.Data) != len(resp.Data) { return fmt.Errorf( "API Middleware number of validators %d does not match gRPC %d", len(respJSON.Data), len(resp.Data), ) } if err = assertValidator(respJSON.Data[0], resp.Data[0]); err != nil { return errors.Wrapf(err, "incorrect validator data returned from the API request") } if err = assertValidator(respJSON.Data[1], resp.Data[1]); err != nil { return errors.Wrapf(err, "incorrect validator data returned from the API request") } return nil } func assertValidator(jsonVal *validatorContainerJson, val *ethpbv1.ValidatorContainer) error { if jsonVal == nil { return errors.New("validator is nil") } if jsonVal.Index != "100" && jsonVal.Index != "200" { return fmt.Errorf("unexpected validator index '%s'", jsonVal.Index) } if jsonVal.Balance != strconv.FormatUint(val.Balance, 10) { return buildFieldError("Balance", strconv.FormatUint(val.Balance, 10), jsonVal.Balance) } if jsonVal.Status != strings.ToLower(val.Status.String()) { return buildFieldError("Status", strings.ToLower(val.Status.String()), jsonVal.Status) } if jsonVal.Validator == nil { return errors.New("validator is nil") } if jsonVal.Validator.PublicKey != hexutil.Encode(val.Validator.Pubkey) { return buildFieldError("PublicKey", hexutil.Encode(val.Validator.Pubkey), jsonVal.Validator.PublicKey) } if jsonVal.Validator.Slashed != val.Validator.Slashed { return buildFieldError("Slashed", strconv.FormatBool(val.Validator.Slashed), strconv.FormatBool(jsonVal.Validator.Slashed)) } return nil } func withCompareSyncCommittee(beaconNodeIdx int, conn *grpc.ClientConn) error { type syncCommitteeValidatorsJson struct { Validators []string `json:"validators"` ValidatorAggregates [][]string `json:"validator_aggregates"` } type syncCommitteesResponseJson struct { Data *syncCommitteeValidatorsJson `json:"data"` } ctx := context.Background() beaconClient := service.NewBeaconChainClient(conn) resp, err := beaconClient.ListSyncCommittees(ctx, ðpbv2.StateSyncCommitteesRequest{ StateId: []byte("head"), }) if err != nil { return err } respJSON := &syncCommitteesResponseJson{} if err := doMiddlewareJSONGetRequestV1( "/beacon/states/head/sync_committees", beaconNodeIdx, respJSON, ); err != nil { return err } if len(respJSON.Data.Validators) != len(resp.Data.Validators) { return fmt.Errorf( "API Middleware number of validators %d does not match gRPC %d", len(respJSON.Data.Validators), len(resp.Data.Validators), ) } if len(respJSON.Data.ValidatorAggregates) != len(resp.Data.ValidatorAggregates) { return fmt.Errorf( "API Middleware number of validator aggregates %d does not match gRPC %d", len(respJSON.Data.ValidatorAggregates), len(resp.Data.ValidatorAggregates), ) } return nil } func withCompareAttesterDuties(beaconNodeIdx int, conn *grpc.ClientConn) error { type attesterDutyJson struct { Pubkey string `json:"pubkey" hex:"true"` ValidatorIndex string `json:"validator_index"` CommitteeIndex string `json:"committee_index"` CommitteeLength string `json:"committee_length"` CommitteesAtSlot string `json:"committees_at_slot"` ValidatorCommitteeIndex string `json:"validator_committee_index"` Slot string `json:"slot"` } type attesterDutiesResponseJson struct { DependentRoot string `json:"dependent_root" hex:"true"` Data []*attesterDutyJson `json:"data"` } ctx := context.Background() validatorClient := service.NewBeaconValidatorClient(conn) resp, err := validatorClient.GetAttesterDuties(ctx, ðpbv1.AttesterDutiesRequest{ Epoch: sharedparams.AltairE2EForkEpoch, Index: []types.ValidatorIndex{0}, }) if err != nil { return err } // We post a top-level array, not an object, as per the spec. reqJSON := []string{"0"} respJSON := &attesterDutiesResponseJson{} if err := doMiddlewareJSONPostRequestV1( "/validator/duties/attester/"+strconv.Itoa(sharedparams.AltairE2EForkEpoch), beaconNodeIdx, reqJSON, respJSON, ); err != nil { return err } if respJSON.DependentRoot != hexutil.Encode(resp.DependentRoot) { return buildFieldError("DependentRoot", string(resp.DependentRoot), respJSON.DependentRoot) } if len(respJSON.Data) != len(resp.Data) { return fmt.Errorf( "API Middleware number of duties %d does not match gRPC %d", len(respJSON.Data), len(resp.Data), ) } return nil } func doMiddlewareJSONGetRequestV1(requestPath string, beaconNodeIdx int, dst interface{}) error { basePath := fmt.Sprintf(v1MiddlewarePathTemplate, params.TestParams.BeaconNodeRPCPort+beaconNodeIdx+40) httpResp, err := http.Get( basePath + requestPath, ) if err != nil { return err } return json.NewDecoder(httpResp.Body).Decode(&dst) } func doMiddlewareJSONPostRequestV1(requestPath string, beaconNodeIdx int, postData interface{}, dst interface{}) error { b, err := json.Marshal(postData) if err != nil { return err } basePath := fmt.Sprintf(v1MiddlewarePathTemplate, params.TestParams.BeaconNodeRPCPort+beaconNodeIdx+40) httpResp, err := http.Post( basePath+requestPath, "application/json", bytes.NewBuffer(b), ) if err != nil { return err } return json.NewDecoder(httpResp.Body).Decode(&dst) } func buildFieldError(field, expected, actual string) error { return fmt.Errorf("value of '%s' was expected to be '%s' but was '%s'", field, expected, actual) }