Redesign of Beacon API evaluator (#13229)

* redesign

* ssz

* small fixes

* capitalize json and ssz

* rename and split files

* clearer names and comments

* bazel fix

* one more simplification
This commit is contained in:
Radosław Kapka
2023-11-30 17:53:51 +01:00
committed by GitHub
parent d8b38cf230
commit e4a5711c8f
9 changed files with 690 additions and 941 deletions

View File

@@ -65,7 +65,7 @@ common_deps = [
"//testing/endtoend/components:go_default_library",
"//testing/endtoend/components/eth1:go_default_library",
"//testing/endtoend/evaluators:go_default_library",
"//testing/endtoend/evaluators/beaconapi_evaluators:go_default_library",
"//testing/endtoend/evaluators/beaconapi:go_default_library",
"//testing/endtoend/helpers:go_default_library",
"//testing/endtoend/params:go_default_library",
"//testing/endtoend/types:go_default_library",

View File

@@ -8,7 +8,7 @@ import (
"github.com/prysmaticlabs/prysm/v4/config/params"
ev "github.com/prysmaticlabs/prysm/v4/testing/endtoend/evaluators"
"github.com/prysmaticlabs/prysm/v4/testing/endtoend/evaluators/beaconapi_evaluators"
"github.com/prysmaticlabs/prysm/v4/testing/endtoend/evaluators/beaconapi"
e2eParams "github.com/prysmaticlabs/prysm/v4/testing/endtoend/params"
"github.com/prysmaticlabs/prysm/v4/testing/endtoend/types"
"github.com/prysmaticlabs/prysm/v4/testing/require"
@@ -166,7 +166,7 @@ func e2eMainnet(t *testing.T, usePrysmSh, useMultiClient bool, cfg *params.Beaco
// In the event we use the cross-client e2e option, we add in an additional
// evaluator for multiclient runs to verify the beacon api conformance.
if testConfig.UseValidatorCrossClient {
testConfig.Evaluators = append(testConfig.Evaluators, beaconapi_evaluators.BeaconAPIMultiClientVerifyIntegrity)
testConfig.Evaluators = append(testConfig.Evaluators, beaconapi.MultiClientVerifyIntegrity)
}
if testConfig.UseBuilder {
testConfig.Evaluators = append(testConfig.Evaluators, ev.BuilderIsActive)

View File

@@ -4,15 +4,15 @@ go_library(
name = "go_default_library",
testonly = True,
srcs = [
"beacon_api.go",
"beacon_api_verify.go",
"requests.go",
"types.go",
"util.go",
"verify.go",
],
importpath = "github.com/prysmaticlabs/prysm/v4/testing/endtoend/evaluators/beaconapi_evaluators",
importpath = "github.com/prysmaticlabs/prysm/v4/testing/endtoend/evaluators/beaconapi",
visibility = ["//testing/endtoend:__subpackages__"],
deps = [
"//api:go_default_library",
"//beacon-chain/rpc/apimiddleware:go_default_library",
"//beacon-chain/rpc/eth/beacon:go_default_library",
"//beacon-chain/rpc/eth/config:go_default_library",
"//beacon-chain/rpc/eth/debug:go_default_library",

View File

@@ -0,0 +1,294 @@
package beaconapi
import (
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/beacon"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/config"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/debug"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/node"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/validator"
"github.com/prysmaticlabs/prysm/v4/config/params"
"github.com/prysmaticlabs/prysm/v4/consensus-types/primitives"
"github.com/prysmaticlabs/prysm/v4/testing/endtoend/helpers"
)
var requests = map[string]endpoint{
"/beacon/genesis": newMetadata[beacon.GetGenesisResponse](v1PathTemplate),
"/beacon/states/{param1}/root": newMetadata[beacon.GetStateRootResponse](v1PathTemplate,
withParams(func(_ primitives.Epoch) []string {
return []string{"head"}
})),
"/beacon/states/{param1}/fork": newMetadata[beacon.GetStateForkResponse](v1PathTemplate,
withParams(func(_ primitives.Epoch) []string {
return []string{"finalized"}
})),
"/beacon/states/{param1}/finality_checkpoints": newMetadata[beacon.GetFinalityCheckpointsResponse](v1PathTemplate,
withParams(func(_ primitives.Epoch) []string {
return []string{"head"}
})),
// we want to test comma-separated query params
"/beacon/states/{param1}/validators?id=0,1": newMetadata[beacon.GetValidatorsResponse](v1PathTemplate,
withParams(func(_ primitives.Epoch) []string {
return []string{"head"}
})),
"/beacon/states/{param1}/validators/{param2}": newMetadata[beacon.GetValidatorResponse](v1PathTemplate,
withParams(func(_ primitives.Epoch) []string {
return []string{"head", "0"}
})),
"/beacon/states/{param1}/validator_balances?id=0,1": newMetadata[beacon.GetValidatorBalancesResponse](v1PathTemplate,
withParams(func(_ primitives.Epoch) []string {
return []string{"head"}
})),
"/beacon/states/{param1}/committees?index=0": newMetadata[beacon.GetCommitteesResponse](v1PathTemplate,
withParams(func(_ primitives.Epoch) []string {
return []string{"head"}
})),
"/beacon/states/{param1}/sync_committees": newMetadata[beacon.GetSyncCommitteeResponse](v1PathTemplate,
withStart(helpers.AltairE2EForkEpoch),
withParams(func(_ primitives.Epoch) []string {
return []string{"head"}
})),
"/beacon/states/{param1}/randao": newMetadata[beacon.GetRandaoResponse](v1PathTemplate,
withParams(func(_ primitives.Epoch) []string {
return []string{"head"}
})),
"/beacon/headers": newMetadata[beacon.GetBlockHeadersResponse](v1PathTemplate),
"/beacon/headers/{param1}": newMetadata[beacon.GetBlockHeaderResponse](v1PathTemplate,
withParams(func(e primitives.Epoch) []string {
slot := uint64(0)
if e > 0 {
slot = (uint64(e) * uint64(params.BeaconConfig().SlotsPerEpoch)) - 1
}
return []string{fmt.Sprintf("%v", slot)}
})),
"/beacon/blocks/{param1}": newMetadata[beacon.GetBlockV2Response](v2PathTemplate,
withSsz(),
withParams(func(e primitives.Epoch) []string {
if e < 4 {
return []string{"head"}
}
return []string{"finalized"}
})),
"/beacon/blocks/{param1}/root": newMetadata[beacon.BlockRootResponse](v1PathTemplate,
withParams(func(_ primitives.Epoch) []string {
return []string{"head"}
})),
"/beacon/blocks/{param1}/attestations": newMetadata[beacon.GetBlockAttestationsResponse](v1PathTemplate,
withParams(func(_ primitives.Epoch) []string {
return []string{"head"}
})),
"/beacon/blinded_blocks/{param1}": newMetadata[beacon.GetBlockV2Response](v1PathTemplate,
withSsz(),
withParams(func(e primitives.Epoch) []string {
if e < 4 {
return []string{"head"}
}
return []string{"finalized"}
})),
"/beacon/pool/attestations": newMetadata[beacon.ListAttestationsResponse](v1PathTemplate,
withCustomEval(func(p interface{}, _ interface{}) error {
pResp, ok := p.(*beacon.ListAttestationsResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &beacon.ListAttestationsResponse{}, p)
}
if pResp.Data == nil {
return errEmptyPrysmData
}
return nil
})),
"/beacon/pool/attester_slashings": newMetadata[beacon.GetAttesterSlashingsResponse](v1PathTemplate,
withCustomEval(func(p interface{}, _ interface{}) error {
pResp, ok := p.(*beacon.GetAttesterSlashingsResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &beacon.GetAttesterSlashingsResponse{}, p)
}
if pResp.Data == nil {
return errEmptyPrysmData
}
return nil
})),
"/beacon/pool/proposer_slashings": newMetadata[beacon.GetProposerSlashingsResponse](v1PathTemplate,
withCustomEval(func(p interface{}, _ interface{}) error {
pResp, ok := p.(*beacon.GetProposerSlashingsResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &beacon.GetProposerSlashingsResponse{}, p)
}
if pResp.Data == nil {
return errEmptyPrysmData
}
return nil
})),
"/beacon/pool/voluntary_exits": newMetadata[beacon.ListVoluntaryExitsResponse](v1PathTemplate,
withCustomEval(func(p interface{}, _ interface{}) error {
pResp, ok := p.(*beacon.ListVoluntaryExitsResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &beacon.ListVoluntaryExitsResponse{}, p)
}
if pResp.Data == nil {
return errEmptyPrysmData
}
return nil
})),
"/beacon/pool/bls_to_execution_changes": newMetadata[beacon.BLSToExecutionChangesPoolResponse](v1PathTemplate,
withCustomEval(func(p interface{}, _ interface{}) error {
pResp, ok := p.(*beacon.BLSToExecutionChangesPoolResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &beacon.BLSToExecutionChangesPoolResponse{}, p)
}
if pResp.Data == nil {
return errEmptyPrysmData
}
return nil
})),
"/config/fork_schedule": newMetadata[config.GetForkScheduleResponse](v1PathTemplate,
withCustomEval(func(p interface{}, lh interface{}) error {
pResp, ok := p.(*config.GetForkScheduleResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &config.GetForkScheduleResponse{}, p)
}
lhResp, ok := lh.(*config.GetForkScheduleResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &config.GetForkScheduleResponse{}, lh)
}
if pResp.Data == nil {
return errEmptyPrysmData
}
if lhResp.Data == nil {
return errEmptyLighthouseData
}
// remove all forks with far-future epoch
for i := len(pResp.Data) - 1; i >= 0; i-- {
if pResp.Data[i].Epoch == fmt.Sprintf("%d", params.BeaconConfig().FarFutureEpoch) {
pResp.Data = append(pResp.Data[:i], pResp.Data[i+1:]...)
}
}
for i := len(lhResp.Data) - 1; i >= 0; i-- {
if lhResp.Data[i].Epoch == fmt.Sprintf("%d", params.BeaconConfig().FarFutureEpoch) {
lhResp.Data = append(lhResp.Data[:i], lhResp.Data[i+1:]...)
}
}
return compareJSON(pResp, lhResp)
})),
"/config/deposit_contract": newMetadata[config.GetDepositContractResponse](v1PathTemplate),
"/debug/beacon/states/{param1}": newMetadata[debug.GetBeaconStateV2Response](v2PathTemplate,
withParams(func(_ primitives.Epoch) []string {
return []string{"head"}
})),
"/debug/beacon/heads": newMetadata[debug.GetForkChoiceHeadsV2Response](v2PathTemplate),
"/node/identity": newMetadata[node.GetIdentityResponse](v1PathTemplate,
withCustomEval(func(p interface{}, _ interface{}) error {
pResp, ok := p.(*node.GetIdentityResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &node.GetIdentityResponse{}, p)
}
if pResp.Data == nil {
return errEmptyPrysmData
}
return nil
})),
"/node/peers": newMetadata[node.GetPeersResponse](v1PathTemplate,
withCustomEval(func(p interface{}, _ interface{}) error {
pResp, ok := p.(*node.GetPeersResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &node.GetPeersResponse{}, p)
}
if pResp.Data == nil {
return errEmptyPrysmData
}
return nil
})),
"/node/peer_count": newMetadata[node.GetPeerCountResponse](v1PathTemplate,
withCustomEval(func(p interface{}, _ interface{}) error {
pResp, ok := p.(*node.GetPeerCountResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &node.GetPeerCountResponse{}, p)
}
if pResp.Data == nil {
return errEmptyPrysmData
}
return nil
})),
"/node/version": newMetadata[node.GetVersionResponse](v1PathTemplate,
withCustomEval(func(p interface{}, lh interface{}) error {
pResp, ok := p.(*node.GetVersionResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &beacon.ListAttestationsResponse{}, p)
}
lhResp, ok := lh.(*node.GetVersionResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &beacon.ListAttestationsResponse{}, p)
}
if pResp.Data == nil {
return errEmptyPrysmData
}
if !strings.Contains(pResp.Data.Version, "Prysm") {
return errors.New("version response does not contain Prysm client name")
}
if lhResp.Data == nil {
return errEmptyLighthouseData
}
if !strings.Contains(lhResp.Data.Version, "Lighthouse") {
return errors.New("version response does not contain Lighthouse client name")
}
return nil
})),
"/node/syncing": newMetadata[node.SyncStatusResponse](v1PathTemplate),
"/validator/duties/proposer/{param1}": newMetadata[validator.GetProposerDutiesResponse](v1PathTemplate,
withParams(func(e primitives.Epoch) []string {
return []string{fmt.Sprintf("%v", e)}
}),
withCustomEval(func(p interface{}, lh interface{}) error {
pResp, ok := p.(*validator.GetProposerDutiesResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &validator.GetProposerDutiesResponse{}, p)
}
lhResp, ok := lh.(*validator.GetProposerDutiesResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &validator.GetProposerDutiesResponse{}, lh)
}
if pResp.Data == nil {
return errEmptyPrysmData
}
if lhResp.Data == nil {
return errEmptyLighthouseData
}
if lhResp.Data[0].Slot == "0" {
// remove the first item from lighthouse data since lighthouse is returning a value despite no proposer
// there is no proposer on slot 0 so prysm don't return anything for slot 0
lhResp.Data = lhResp.Data[1:]
}
return compareJSON(pResp, lhResp)
})),
"/validator/duties/attester/{param1}": newMetadata[validator.GetAttesterDutiesResponse](v1PathTemplate,
withParams(func(e primitives.Epoch) []string {
//ask for a future epoch to test this case
return []string{fmt.Sprintf("%v", e+1)}
}),
withReq(func() []string {
validatorIndices := make([]string, 64)
for key := range validatorIndices {
validatorIndices[key] = fmt.Sprintf("%d", key)
}
return validatorIndices
}()),
withCustomEval(func(p interface{}, lh interface{}) error {
pResp, ok := p.(*validator.GetAttesterDutiesResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &validator.GetAttesterDutiesResponse{}, p)
}
lhResp, ok := lh.(*validator.GetAttesterDutiesResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &validator.GetAttesterDutiesResponse{}, lh)
}
if pResp.Data == nil {
return errEmptyPrysmData
}
if lhResp.Data == nil {
return errEmptyLighthouseData
}
return compareJSON(pResp, lhResp)
})),
}

View File

@@ -0,0 +1,140 @@
package beaconapi
import "github.com/prysmaticlabs/prysm/v4/consensus-types/primitives"
type endpoint interface {
getBasePath() string
sszEnabled() bool
enableSsz()
getSszResp() []byte // retrieves the Prysm SSZ response
setSszResp(resp []byte) // sets the Prysm SSZ response
getStart() primitives.Epoch
setStart(start primitives.Epoch)
getReq() interface{}
setReq(req interface{})
getPResp() interface{} // retrieves the Prysm JSON response
getLHResp() interface{} // retrieves the Lighthouse JSON response
getParams(epoch primitives.Epoch) []string
setParams(f func(primitives.Epoch) []string)
getCustomEval() func(interface{}, interface{}) error
setCustomEval(f func(interface{}, interface{}) error)
}
type apiEndpoint[Resp any] struct {
basePath string
ssz bool
start primitives.Epoch
req interface{}
pResp *Resp // Prysm JSON response
lhResp *Resp // Lighthouse JSON response
sszResp []byte // Prysm SSZ response
params func(currentEpoch primitives.Epoch) []string
customEval func(interface{}, interface{}) error
}
func (e *apiEndpoint[Resp]) getBasePath() string {
return e.basePath
}
func (e *apiEndpoint[Resp]) sszEnabled() bool {
return e.ssz
}
func (e *apiEndpoint[Resp]) enableSsz() {
e.ssz = true
}
func (e *apiEndpoint[Resp]) getSszResp() []byte {
return e.sszResp
}
func (e *apiEndpoint[Resp]) setSszResp(resp []byte) {
e.sszResp = resp
}
func (e *apiEndpoint[Resp]) getStart() primitives.Epoch {
return e.start
}
func (e *apiEndpoint[Resp]) setStart(start primitives.Epoch) {
e.start = start
}
func (e *apiEndpoint[Resp]) getReq() interface{} {
return e.req
}
func (e *apiEndpoint[Resp]) setReq(req interface{}) {
e.req = req
}
func (e *apiEndpoint[Resp]) getPResp() interface{} {
return e.pResp
}
func (e *apiEndpoint[Resp]) getLHResp() interface{} {
return e.lhResp
}
func (e *apiEndpoint[Resp]) getParams(epoch primitives.Epoch) []string {
if e.params == nil {
return nil
}
return e.params(epoch)
}
func (e *apiEndpoint[Resp]) setParams(f func(currentEpoch primitives.Epoch) []string) {
e.params = f
}
func (e *apiEndpoint[Resp]) getCustomEval() func(interface{}, interface{}) error {
return e.customEval
}
func (e *apiEndpoint[Resp]) setCustomEval(f func(interface{}, interface{}) error) {
e.customEval = f
}
func newMetadata[Resp any](basePath string, opts ...endpointOpt) *apiEndpoint[Resp] {
m := &apiEndpoint[Resp]{
basePath: basePath,
pResp: new(Resp),
lhResp: new(Resp),
}
for _, o := range opts {
o(m)
}
return m
}
type endpointOpt func(endpoint)
func withSsz() endpointOpt {
return func(e endpoint) {
e.enableSsz()
}
}
func withStart(start primitives.Epoch) endpointOpt {
return func(e endpoint) {
e.setStart(start)
}
}
func withReq(req interface{}) endpointOpt {
return func(e endpoint) {
e.setReq(req)
}
}
func withParams(f func(currentEpoch primitives.Epoch) []string) endpointOpt {
return func(e endpoint) {
e.setParams(f)
}
}
func withCustomEval(f func(interface{}, interface{}) error) endpointOpt {
return func(e endpoint) {
e.setCustomEval(f)
}
}

View File

@@ -1,4 +1,4 @@
package beaconapi_evaluators
package beaconapi
import (
"bytes"
@@ -7,12 +7,26 @@ import (
"io"
"net/http"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v4/api"
"github.com/prysmaticlabs/prysm/v4/testing/endtoend/params"
log "github.com/sirupsen/logrus"
)
func doJSONGetRequest(template string, requestPath string, beaconNodeIdx int, dst interface{}, bnType ...string) error {
var (
errSszCast = errors.New("SSZ response is not a byte array")
errEmptyPrysmData = errors.New("Prysm data is empty")
errEmptyLighthouseData = errors.New("Lighthouse data is empty")
)
const (
msgWrongJson = "JSON response has wrong structure, expected %T, got %T"
msgRequestFailed = "%s request failed with response code %d with response body %s"
msgUnknownNode = "unknown node type %s"
msgSSZUnmarshalFailed = "failed to unmarshal SSZ"
)
func doJSONGetRequest(template string, requestPath string, beaconNodeIdx int, resp interface{}, bnType ...string) error {
if len(bnType) == 0 {
bnType = []string{"prysm"}
}
@@ -24,7 +38,7 @@ func doJSONGetRequest(template string, requestPath string, beaconNodeIdx int, ds
case "lighthouse":
port = params.TestParams.Ports.LighthouseBeaconNodeHTTPPort
default:
return fmt.Errorf("unknown node type %s", bnType[0])
return fmt.Errorf(msgUnknownNode, bnType[0])
}
basePath := fmt.Sprintf(template, port+beaconNodeIdx)
@@ -47,9 +61,9 @@ func doJSONGetRequest(template string, requestPath string, beaconNodeIdx int, ds
return err
}
}
return fmt.Errorf("%s request failed with response code %d with response body %s", bnType[0], httpResp.StatusCode, body)
return fmt.Errorf(msgRequestFailed, bnType[0], httpResp.StatusCode, body)
}
return json.NewDecoder(httpResp.Body).Decode(&dst)
return json.NewDecoder(httpResp.Body).Decode(&resp)
}
func doSSZGetRequest(template string, requestPath string, beaconNodeIdx int, bnType ...string) ([]byte, error) {
@@ -65,7 +79,7 @@ func doSSZGetRequest(template string, requestPath string, beaconNodeIdx int, bnT
case "lighthouse":
port = params.TestParams.Ports.LighthouseBeaconNodeHTTPPort
default:
return nil, fmt.Errorf("unknown node type %s", bnType[0])
return nil, fmt.Errorf(msgUnknownNode, bnType[0])
}
basePath := fmt.Sprintf(template, port+beaconNodeIdx)
@@ -84,7 +98,7 @@ func doSSZGetRequest(template string, requestPath string, beaconNodeIdx int, bnT
if err := json.NewDecoder(rsp.Body).Decode(&body); err != nil {
return nil, err
}
return nil, fmt.Errorf("%s request failed with response code: %d with response body %s", bnType[0], rsp.StatusCode, body)
return nil, fmt.Errorf(msgRequestFailed, bnType[0], rsp.StatusCode, body)
}
defer closeBody(rsp.Body)
body, err := io.ReadAll(rsp.Body)
@@ -95,7 +109,7 @@ func doSSZGetRequest(template string, requestPath string, beaconNodeIdx int, bnT
return body, nil
}
func doJSONPostRequest(template string, requestPath string, beaconNodeIdx int, postData, dst interface{}, bnType ...string) error {
func doJSONPostRequest(template string, requestPath string, beaconNodeIdx int, postData, resp interface{}, bnType ...string) error {
if len(bnType) == 0 {
bnType = []string{"prysm"}
}
@@ -107,7 +121,7 @@ func doJSONPostRequest(template string, requestPath string, beaconNodeIdx int, p
case "lighthouse":
port = params.TestParams.Ports.LighthouseBeaconNodeHTTPPort
default:
return fmt.Errorf("unknown node type %s", bnType[0])
return fmt.Errorf(msgUnknownNode, bnType[0])
}
basePath := fmt.Sprintf(template, port+beaconNodeIdx)
@@ -136,9 +150,9 @@ func doJSONPostRequest(template string, requestPath string, beaconNodeIdx int, p
return err
}
}
return fmt.Errorf("%s request failed with response code %d with response body %s", bnType[0], httpResp.StatusCode, body)
return fmt.Errorf(msgRequestFailed, bnType[0], httpResp.StatusCode, body)
}
return json.NewDecoder(httpResp.Body).Decode(&dst)
return json.NewDecoder(httpResp.Body).Decode(&resp)
}
func closeBody(body io.Closer) {

View File

@@ -0,0 +1,224 @@
package beaconapi
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/beacon"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/validator"
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v4/testing/endtoend/helpers"
"github.com/prysmaticlabs/prysm/v4/testing/endtoend/policies"
e2etypes "github.com/prysmaticlabs/prysm/v4/testing/endtoend/types"
"github.com/prysmaticlabs/prysm/v4/time/slots"
"google.golang.org/grpc"
)
// MultiClientVerifyIntegrity tests Beacon API endpoints.
// It compares responses from Prysm and other beacon nodes such as Lighthouse.
// The evaluator is executed on every odd-numbered epoch.
var MultiClientVerifyIntegrity = e2etypes.Evaluator{
Name: "beacon_api_multi-client_verify_integrity_epoch_%d",
Policy: policies.EveryNEpochs(1, 2),
Evaluation: verify,
}
const (
v1PathTemplate = "http://localhost:%d/eth/v1"
v2PathTemplate = "http://localhost:%d/eth/v2"
)
func verify(_ *e2etypes.EvaluationContext, conns ...*grpc.ClientConn) error {
for beaconNodeIdx := range conns {
if err := run(beaconNodeIdx); err != nil {
return err
}
}
return nil
}
func run(nodeIdx int) error {
genesisResp := &beacon.GetGenesisResponse{}
if err := doJSONGetRequest(v1PathTemplate, "/beacon/genesis", nodeIdx, genesisResp); err != nil {
return errors.Wrap(err, "error getting genesis data")
}
genesisTime, err := strconv.ParseInt(genesisResp.Data.GenesisTime, 10, 64)
if err != nil {
return errors.Wrap(err, "could not parse genesis time")
}
currentEpoch := slots.EpochsSinceGenesis(time.Unix(genesisTime, 0))
for path, m := range requests {
if currentEpoch < m.getStart() {
continue
}
apiPath := path
if m.getParams(currentEpoch) != nil {
apiPath = pathFromParams(path, m.getParams(currentEpoch))
}
fmt.Printf("executing JSON path: %s\n", apiPath)
if err = compareJSONMultiClient(nodeIdx, m.getBasePath(), apiPath, m.getReq(), m.getPResp(), m.getLHResp(), m.getCustomEval()); err != nil {
return err
}
if m.sszEnabled() {
fmt.Printf("executing SSZ path: %s\n", apiPath)
b, err := compareSSZMultiClient(nodeIdx, m.getBasePath(), apiPath)
if err != nil {
return err
}
m.setSszResp(b)
}
}
return postEvaluation(requests)
}
// postEvaluation performs additional evaluation after all requests have been completed.
// It is useful for things such as checking if specific fields match between endpoints.
func postEvaluation(requests map[string]endpoint) error {
// verify that block SSZ responses have the correct structure
forkData := requests["/beacon/states/{param1}/fork"]
fork, ok := forkData.getPResp().(*beacon.GetStateForkResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &beacon.GetStateForkResponse{}, forkData.getPResp())
}
finalizedEpoch, err := strconv.ParseUint(fork.Data.Epoch, 10, 64)
if err != nil {
return err
}
blockData := requests["/beacon/blocks/{param1}"]
blindedBlockData := requests["/beacon/blinded_blocks/{param1}"]
if !ok {
return errSszCast
}
if finalizedEpoch < helpers.AltairE2EForkEpoch+2 {
b := &ethpb.SignedBeaconBlock{}
if err := b.UnmarshalSSZ(blockData.getSszResp()); err != nil {
return errors.Wrap(err, msgSSZUnmarshalFailed)
}
bb := &ethpb.SignedBeaconBlock{}
if err := bb.UnmarshalSSZ(blindedBlockData.getSszResp()); err != nil {
return errors.Wrap(err, msgSSZUnmarshalFailed)
}
} else if finalizedEpoch >= helpers.AltairE2EForkEpoch+2 && finalizedEpoch < helpers.BellatrixE2EForkEpoch {
b := &ethpb.SignedBeaconBlockAltair{}
if err := b.UnmarshalSSZ(blockData.getSszResp()); err != nil {
return errors.Wrap(err, msgSSZUnmarshalFailed)
}
bb := &ethpb.SignedBeaconBlockAltair{}
if err := bb.UnmarshalSSZ(blindedBlockData.getSszResp()); err != nil {
return errors.Wrap(err, msgSSZUnmarshalFailed)
}
} else if finalizedEpoch >= helpers.BellatrixE2EForkEpoch && finalizedEpoch < helpers.CapellaE2EForkEpoch {
b := &ethpb.SignedBeaconBlockBellatrix{}
if err := b.UnmarshalSSZ(blockData.getSszResp()); err != nil {
return errors.Wrap(err, msgSSZUnmarshalFailed)
}
bb := &ethpb.SignedBlindedBeaconBlockBellatrix{}
if err := bb.UnmarshalSSZ(blindedBlockData.getSszResp()); err != nil {
return errors.Wrap(err, msgSSZUnmarshalFailed)
}
} else if finalizedEpoch >= helpers.CapellaE2EForkEpoch && finalizedEpoch < helpers.DenebE2EForkEpoch {
b := &ethpb.SignedBeaconBlockCapella{}
if err := b.UnmarshalSSZ(blockData.getSszResp()); err != nil {
return errors.Wrap(err, msgSSZUnmarshalFailed)
}
bb := &ethpb.SignedBlindedBeaconBlockCapella{}
if err := bb.UnmarshalSSZ(blindedBlockData.getSszResp()); err != nil {
return errors.Wrap(err, msgSSZUnmarshalFailed)
}
} else {
b := &ethpb.SignedBeaconBlockDeneb{}
if err := b.UnmarshalSSZ(blockData.getSszResp()); err != nil {
return errors.Wrap(err, msgSSZUnmarshalFailed)
}
bb := &ethpb.SignedBlindedBeaconBlockDeneb{}
if err := bb.UnmarshalSSZ(blindedBlockData.getSszResp()); err != nil {
return errors.Wrap(err, msgSSZUnmarshalFailed)
}
}
// verify that dependent root of proposer duties matches block header
blockHeaderData := requests["/beacon/headers/{param1}"]
header, ok := blockHeaderData.getPResp().(*beacon.GetBlockHeaderResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &beacon.GetBlockHeaderResponse{}, blockHeaderData.getPResp())
}
dutiesData := requests["/validator/duties/proposer/{param1}"]
duties, ok := dutiesData.getPResp().(*validator.GetProposerDutiesResponse)
if !ok {
return fmt.Errorf(msgWrongJson, &validator.GetProposerDutiesResponse{}, dutiesData.getPResp())
}
if header.Data.Root != duties.DependentRoot {
return fmt.Errorf("header root %s does not match duties root %s ", header.Data.Root, duties.DependentRoot)
}
return nil
}
func compareJSONMultiClient(nodeIdx int, base, path string, req, pResp, lhResp interface{}, customEval func(interface{}, interface{}) error) error {
if req != nil {
if err := doJSONPostRequest(base, path, nodeIdx, req, pResp); err != nil {
return errors.Wrapf(err, "could not perform Prysm JSON POST request for path %s", path)
}
if err := doJSONPostRequest(base, path, nodeIdx, req, lhResp, "lighthouse"); err != nil {
return errors.Wrapf(err, "could not perform Lighthouse JSON POST request for path %s", path)
}
} else {
if err := doJSONGetRequest(base, path, nodeIdx, pResp); err != nil {
return errors.Wrapf(err, "could not perform Prysm JSON GET request for path %s", path)
}
if err := doJSONGetRequest(base, path, nodeIdx, lhResp, "lighthouse"); err != nil {
return errors.Wrapf(err, "could not perform Lighthouse JSON GET request for path %s", path)
}
}
if customEval != nil {
return customEval(pResp, lhResp)
} else {
return compareJSON(pResp, lhResp)
}
}
func compareSSZMultiClient(nodeIdx int, base, path string) ([]byte, error) {
pResp, err := doSSZGetRequest(base, path, nodeIdx)
if err != nil {
return nil, errors.Wrapf(err, "could not perform Prysm SSZ GET request for path %s", path)
}
lhResp, err := doSSZGetRequest(base, path, nodeIdx, "lighthouse")
if err != nil {
return nil, errors.Wrapf(err, "could not perform Lighthouse SSZ GET request for path %s", path)
}
if !bytes.Equal(pResp, lhResp) {
return nil, errors.New("Prysm SSZ response does not match Lighthouse SSZ response")
}
return pResp, nil
}
func compareJSON(pResp interface{}, lhResp interface{}) error {
if !reflect.DeepEqual(pResp, lhResp) {
p, err := json.Marshal(pResp)
if err != nil {
return errors.Wrap(err, "failed to marshal Prysm response to JSON")
}
lh, err := json.Marshal(lhResp)
if err != nil {
return errors.Wrap(err, "failed to marshal Lighthouse response to JSON")
}
return fmt.Errorf("Prysm response %s does not match Lighthouse response %s", string(p), string(lh))
}
return nil
}
func pathFromParams(path string, params []string) string {
apiPath := path
for i := range params {
apiPath = strings.Replace(apiPath, fmt.Sprintf("{param%d}", i+1), params[i], 1)
}
return apiPath
}

View File

@@ -1,876 +0,0 @@
package beaconapi_evaluators
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/apimiddleware"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/beacon"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/config"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/debug"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/node"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/validator"
"github.com/prysmaticlabs/prysm/v4/config/params"
"github.com/prysmaticlabs/prysm/v4/consensus-types/primitives"
ethpb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
"github.com/prysmaticlabs/prysm/v4/testing/endtoend/helpers"
"github.com/prysmaticlabs/prysm/v4/time/slots"
)
var (
errSszCast = errors.New("ssz response is not a byte array")
errJsonCast = errors.New("json response has wrong structure")
errEmptyPrysmData = errors.New("prysm data is empty")
errEmptyLighthouseData = errors.New("lighthouse data is empty")
)
type metadata struct {
start primitives.Epoch
basepath string
params func(encoding string, currentEpoch primitives.Epoch) []string
requestObject interface{}
prysmResps map[string]interface{}
lighthouseResps map[string]interface{}
customEvaluation func(interface{}, interface{}) error
}
var requests = map[string]metadata{
"/beacon/genesis": {
basepath: v1PathTemplate,
prysmResps: map[string]interface{}{
"json": &beacon.GetGenesisResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetGenesisResponse{},
},
},
"/beacon/states/{param1}/root": {
basepath: v1PathTemplate,
params: func(_ string, _ primitives.Epoch) []string {
return []string{"head"}
},
prysmResps: map[string]interface{}{
"json": &beacon.GetStateRootResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetStateRootResponse{},
},
},
"/beacon/states/{param1}/fork": {
basepath: v1PathTemplate,
params: func(_ string, _ primitives.Epoch) []string {
return []string{"finalized"}
},
prysmResps: map[string]interface{}{
"json": &beacon.GetStateForkResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetStateForkResponse{},
},
},
"/beacon/states/{param1}/finality_checkpoints": {
basepath: v1PathTemplate,
params: func(_ string, _ primitives.Epoch) []string {
return []string{"head"}
},
prysmResps: map[string]interface{}{
"json": &beacon.GetFinalityCheckpointsResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetFinalityCheckpointsResponse{},
},
},
// we want to test comma-separated query params
"/beacon/states/{param1}/validators?id=0,1": {
basepath: v1PathTemplate,
params: func(_ string, _ primitives.Epoch) []string {
return []string{"head"}
},
prysmResps: map[string]interface{}{
"json": &beacon.GetValidatorsResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetValidatorsResponse{},
},
},
"/beacon/states/{param1}/validators/{param2}": {
basepath: v1PathTemplate,
params: func(_ string, _ primitives.Epoch) []string {
return []string{"head", "0"}
},
prysmResps: map[string]interface{}{
"json": &beacon.GetValidatorResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetValidatorResponse{},
},
},
"/beacon/states/{param1}/validator_balances?id=0,1": {
basepath: v1PathTemplate,
params: func(_ string, _ primitives.Epoch) []string {
return []string{"head"}
},
prysmResps: map[string]interface{}{
"json": &beacon.GetValidatorBalancesResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetValidatorBalancesResponse{},
},
},
"/beacon/states/{param1}/committees?index=0": {
basepath: v1PathTemplate,
params: func(_ string, _ primitives.Epoch) []string {
return []string{"head"}
},
prysmResps: map[string]interface{}{
"json": &beacon.GetCommitteesResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetCommitteesResponse{},
},
},
"/beacon/states/{param1}/sync_committees": {
start: helpers.AltairE2EForkEpoch,
basepath: v1PathTemplate,
params: func(_ string, _ primitives.Epoch) []string {
return []string{"head"}
},
prysmResps: map[string]interface{}{
"json": &beacon.GetSyncCommitteeResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetSyncCommitteeResponse{},
},
},
"/beacon/states/{param1}/randao": {
basepath: v1PathTemplate,
params: func(_ string, _ primitives.Epoch) []string {
return []string{"head"}
},
prysmResps: map[string]interface{}{
"json": &beacon.GetRandaoResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetRandaoResponse{},
},
},
"/beacon/headers": {
basepath: v1PathTemplate,
prysmResps: map[string]interface{}{
"json": &beacon.GetBlockHeadersResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetBlockHeadersResponse{},
},
},
"/beacon/headers/{param1}": {
basepath: v1PathTemplate,
params: func(_ string, e primitives.Epoch) []string {
slot := uint64(0)
if e > 0 {
slot = (uint64(e) * uint64(params.BeaconConfig().SlotsPerEpoch)) - 1
}
return []string{fmt.Sprintf("%v", slot)}
},
prysmResps: map[string]interface{}{
"json": &beacon.GetBlockHeaderResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetBlockHeaderResponse{},
},
},
"/beacon/blocks/{param1}": {
basepath: v2PathTemplate,
params: func(t string, e primitives.Epoch) []string {
if t == "ssz" {
if e < 4 {
return []string{"genesis"}
}
return []string{"finalized"}
}
return []string{"head"}
},
prysmResps: map[string]interface{}{
"json": &beacon.GetBlockV2Response{},
"ssz": []byte{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetBlockV2Response{},
"ssz": []byte{},
},
},
"/beacon/blocks/{param1}/root": {
basepath: v1PathTemplate,
params: func(_ string, _ primitives.Epoch) []string {
return []string{"head"}
},
prysmResps: map[string]interface{}{
"json": &beacon.BlockRootResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.BlockRootResponse{},
},
},
"/beacon/blocks/{param1}/attestations": {
basepath: v1PathTemplate,
params: func(_ string, _ primitives.Epoch) []string {
return []string{"head"}
},
prysmResps: map[string]interface{}{
"json": &beacon.GetBlockAttestationsResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetBlockAttestationsResponse{},
},
},
"/beacon/blinded_blocks/{param1}": {
basepath: v1PathTemplate,
params: func(t string, e primitives.Epoch) []string {
if t == "ssz" {
if e < 4 {
return []string{"genesis"}
}
return []string{"finalized"}
}
return []string{"head"}
},
prysmResps: map[string]interface{}{
"json": &beacon.GetBlockV2Response{},
"ssz": []byte{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetBlockV2Response{},
"ssz": []byte{},
},
},
"/beacon/pool/attestations": {
basepath: v1PathTemplate,
prysmResps: map[string]interface{}{
"json": &beacon.ListAttestationsResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.ListAttestationsResponse{},
},
customEvaluation: func(p interface{}, l interface{}) error {
pResp, ok := p.(*beacon.ListAttestationsResponse)
if !ok {
return errJsonCast
}
lResp, ok := l.(*beacon.ListAttestationsResponse)
if !ok {
return errJsonCast
}
if pResp.Data == nil {
return errEmptyPrysmData
}
if lResp.Data == nil {
return errEmptyLighthouseData
}
return nil
},
},
"/beacon/pool/attester_slashings": {
basepath: v1PathTemplate,
prysmResps: map[string]interface{}{
"json": &beacon.GetAttesterSlashingsResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetAttesterSlashingsResponse{},
},
customEvaluation: func(p interface{}, l interface{}) error {
pResp, ok := p.(*beacon.GetAttesterSlashingsResponse)
if !ok {
return errJsonCast
}
lResp, ok := l.(*beacon.GetAttesterSlashingsResponse)
if !ok {
return errJsonCast
}
if pResp.Data == nil {
return errEmptyPrysmData
}
if lResp.Data == nil {
return errEmptyLighthouseData
}
return nil
},
},
"/beacon/pool/proposer_slashings": {
basepath: v1PathTemplate,
prysmResps: map[string]interface{}{
"json": &beacon.GetProposerSlashingsResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetProposerSlashingsResponse{},
},
customEvaluation: func(p interface{}, l interface{}) error {
pResp, ok := p.(*beacon.GetProposerSlashingsResponse)
if !ok {
return errJsonCast
}
lResp, ok := l.(*beacon.GetProposerSlashingsResponse)
if !ok {
return errJsonCast
}
if pResp.Data == nil {
return errEmptyPrysmData
}
if lResp.Data == nil {
return errEmptyLighthouseData
}
return nil
},
},
"/beacon/pool/voluntary_exits": {
basepath: v1PathTemplate,
prysmResps: map[string]interface{}{
"json": &beacon.ListVoluntaryExitsResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.ListVoluntaryExitsResponse{},
},
customEvaluation: func(p interface{}, l interface{}) error {
pResp, ok := p.(*beacon.ListVoluntaryExitsResponse)
if !ok {
return errJsonCast
}
lResp, ok := l.(*beacon.ListVoluntaryExitsResponse)
if !ok {
return errJsonCast
}
if pResp.Data == nil {
return errEmptyPrysmData
}
if lResp.Data == nil {
return errEmptyLighthouseData
}
return nil
},
},
"/beacon/pool/bls_to_execution_changes": {
basepath: v1PathTemplate,
prysmResps: map[string]interface{}{
"json": &beacon.BLSToExecutionChangesPoolResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.BLSToExecutionChangesPoolResponse{},
},
customEvaluation: func(p interface{}, l interface{}) error {
pResp, ok := p.(*beacon.BLSToExecutionChangesPoolResponse)
if !ok {
return errJsonCast
}
lResp, ok := l.(*beacon.BLSToExecutionChangesPoolResponse)
if !ok {
return errJsonCast
}
if pResp.Data == nil {
return errEmptyPrysmData
}
if lResp.Data == nil {
return errEmptyLighthouseData
}
return nil
},
},
"/config/fork_schedule": {
basepath: v1PathTemplate,
prysmResps: map[string]interface{}{
"json": &config.GetForkScheduleResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &config.GetForkScheduleResponse{},
},
customEvaluation: func(p interface{}, l interface{}) error {
// remove all forks with far-future epoch
pSchedule, ok := p.(*config.GetForkScheduleResponse)
if !ok {
return errJsonCast
}
for i := len(pSchedule.Data) - 1; i >= 0; i-- {
if pSchedule.Data[i].Epoch == fmt.Sprintf("%d", params.BeaconConfig().FarFutureEpoch) {
pSchedule.Data = append(pSchedule.Data[:i], pSchedule.Data[i+1:]...)
}
}
lSchedule, ok := l.(*config.GetForkScheduleResponse)
if !ok {
return errJsonCast
}
for i := len(lSchedule.Data) - 1; i >= 0; i-- {
if lSchedule.Data[i].Epoch == fmt.Sprintf("%d", params.BeaconConfig().FarFutureEpoch) {
lSchedule.Data = append(lSchedule.Data[:i], lSchedule.Data[i+1:]...)
}
}
return compareJSONResponseObjects(pSchedule, lSchedule)
},
},
"/config/deposit_contract": {
basepath: v1PathTemplate,
prysmResps: map[string]interface{}{
"json": &apimiddleware.DepositContractResponseJson{},
},
lighthouseResps: map[string]interface{}{
"json": &apimiddleware.DepositContractResponseJson{},
},
},
"/debug/beacon/states/{param1}": {
basepath: v2PathTemplate,
params: func(_ string, _ primitives.Epoch) []string {
return []string{"head"}
},
prysmResps: map[string]interface{}{
"json": &debug.GetBeaconStateV2Response{},
},
lighthouseResps: map[string]interface{}{
"json": &debug.GetBeaconStateV2Response{},
},
},
"/debug/beacon/heads": {
basepath: v2PathTemplate,
prysmResps: map[string]interface{}{
"json": &debug.GetForkChoiceHeadsV2Response{},
},
lighthouseResps: map[string]interface{}{
"json": &debug.GetForkChoiceHeadsV2Response{},
},
},
"/node/identity": {
basepath: v1PathTemplate,
prysmResps: map[string]interface{}{
"json": &node.GetIdentityResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &node.GetIdentityResponse{},
},
customEvaluation: func(p interface{}, l interface{}) error {
pResp, ok := p.(*node.GetIdentityResponse)
if !ok {
return errJsonCast
}
lResp, ok := l.(*node.GetIdentityResponse)
if !ok {
return errJsonCast
}
if pResp.Data == nil {
return errEmptyPrysmData
}
if lResp.Data == nil {
return errEmptyLighthouseData
}
return nil
},
},
"/node/peers": {
basepath: v1PathTemplate,
prysmResps: map[string]interface{}{
"json": &node.GetPeersResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &node.GetPeersResponse{},
},
customEvaluation: func(p interface{}, l interface{}) error {
pResp, ok := p.(*node.GetPeersResponse)
if !ok {
return errJsonCast
}
lResp, ok := l.(*node.GetPeersResponse)
if !ok {
return errJsonCast
}
if pResp.Data == nil {
return errEmptyPrysmData
}
if lResp.Data == nil {
return errEmptyLighthouseData
}
return nil
},
},
"/node/peer_count": {
basepath: v1PathTemplate,
prysmResps: map[string]interface{}{
"json": &node.GetPeerCountResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &node.GetPeerCountResponse{},
},
customEvaluation: func(p interface{}, l interface{}) error {
pResp, ok := p.(*node.GetPeerCountResponse)
if !ok {
return errJsonCast
}
lResp, ok := l.(*node.GetPeerCountResponse)
if !ok {
return errJsonCast
}
if pResp.Data == nil {
return errEmptyPrysmData
}
if lResp.Data == nil {
return errEmptyLighthouseData
}
return nil
},
},
"/node/version": {
basepath: v1PathTemplate,
prysmResps: map[string]interface{}{
"json": &node.GetVersionResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &node.GetVersionResponse{},
},
customEvaluation: func(p interface{}, l interface{}) error {
pResp, ok := p.(*node.GetVersionResponse)
if !ok {
return errJsonCast
}
lResp, ok := l.(*node.GetVersionResponse)
if !ok {
return errJsonCast
}
if pResp.Data == nil {
return errEmptyPrysmData
}
if !strings.Contains(pResp.Data.Version, "Prysm") {
return errors.New("version response does not contain Prysm client name")
}
if lResp.Data == nil {
return errEmptyLighthouseData
}
if !strings.Contains(lResp.Data.Version, "Lighthouse") {
return errors.New("version response does not contain Lighthouse client name")
}
return nil
},
},
"/node/syncing": {
basepath: v1PathTemplate,
prysmResps: map[string]interface{}{
"json": &node.SyncStatusResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &node.SyncStatusResponse{},
},
},
"/validator/duties/proposer/{param1}": {
basepath: v1PathTemplate,
params: func(_ string, e primitives.Epoch) []string {
return []string{fmt.Sprintf("%v", e)}
},
prysmResps: map[string]interface{}{
"json": &validator.GetProposerDutiesResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &validator.GetProposerDutiesResponse{},
},
customEvaluation: func(prysmResp interface{}, lhouseResp interface{}) error {
castedl, ok := lhouseResp.(*validator.GetProposerDutiesResponse)
if !ok {
return errors.New("failed to cast type")
}
if castedl.Data[0].Slot == "0" {
// remove the first item from lighthouse data since lighthouse is returning a value despite no proposer
// there is no proposer on slot 0 so prysm don't return anything for slot 0
castedl.Data = castedl.Data[1:]
}
return compareJSONResponseObjects(prysmResp, castedl)
},
},
"/validator/duties/attester/{param1}": {
basepath: v1PathTemplate,
params: func(_ string, e primitives.Epoch) []string {
//ask for a future epoch to test this case
return []string{fmt.Sprintf("%v", e+1)}
},
requestObject: func() []string {
validatorIndices := make([]string, 64)
for key := range validatorIndices {
validatorIndices[key] = fmt.Sprintf("%d", key)
}
return validatorIndices
}(),
prysmResps: map[string]interface{}{
"json": &validator.GetAttesterDutiesResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &validator.GetAttesterDutiesResponse{},
},
customEvaluation: func(prysmResp interface{}, lhouseResp interface{}) error {
castedp, ok := lhouseResp.(*validator.GetAttesterDutiesResponse)
if !ok {
return errors.New("failed to cast type")
}
castedl, ok := lhouseResp.(*validator.GetAttesterDutiesResponse)
if !ok {
return errors.New("failed to cast type")
}
if len(castedp.Data) == 0 ||
len(castedl.Data) == 0 ||
len(castedp.Data) != len(castedl.Data) {
return fmt.Errorf("attester data does not match, prysm: %d lighthouse: %d", len(castedp.Data), len(castedl.Data))
}
return compareJSONResponseObjects(prysmResp, castedl)
},
},
}
func withCompareBeaconAPIs(beaconNodeIdx int) error {
genesisResp := &beacon.GetGenesisResponse{}
err := doJSONGetRequest(
v1PathTemplate,
"/beacon/genesis",
beaconNodeIdx,
genesisResp,
)
if err != nil {
return errors.Wrap(err, "error getting genesis data")
}
genesisTime, err := strconv.ParseInt(genesisResp.Data.GenesisTime, 10, 64)
if err != nil {
return errors.Wrap(err, "could not parse genesis time")
}
currentEpoch := slots.EpochsSinceGenesis(time.Unix(genesisTime, 0))
for path, meta := range requests {
if currentEpoch < meta.start {
continue
}
for key := range meta.prysmResps {
switch key {
case "json":
apipath := path
if meta.params != nil {
jsonparams := meta.params("json", currentEpoch)
apipath = pathFromParams(path, jsonparams)
}
fmt.Printf("executing json api path: %s\n", apipath)
if err := compareJSONMulticlient(beaconNodeIdx,
meta.basepath,
apipath,
meta.requestObject,
requests[path].prysmResps[key],
requests[path].lighthouseResps[key],
meta.customEvaluation,
); err != nil {
return err
}
case "ssz":
apipath := path
if meta.params != nil {
sszparams := meta.params("ssz", currentEpoch)
apipath = pathFromParams(path, sszparams)
}
fmt.Printf("executing ssz api path: %s\n", apipath)
prysmr, lighthouser, err := compareSSZMulticlient(beaconNodeIdx, meta.basepath, apipath)
if err != nil {
return err
}
requests[path].prysmResps[key] = prysmr
requests[path].lighthouseResps[key] = lighthouser
default:
return fmt.Errorf("unknown encoding type %s", key)
}
}
}
return postEvaluation(beaconNodeIdx, requests)
}
// postEvaluation performs additional evaluation after all requests have been completed.
// It is useful for things such as checking if specific fields match between endpoints.
func postEvaluation(beaconNodeIdx int, requests map[string]metadata) error {
// verify that block SSZ responses have the correct structure
forkData := requests["/beacon/states/{param1}/fork"]
fork, ok := forkData.prysmResps["json"].(*beacon.GetStateForkResponse)
if !ok {
return errJsonCast
}
finalizedEpoch, err := strconv.ParseUint(fork.Data.Epoch, 10, 64)
if err != nil {
return err
}
blockData := requests["/beacon/blocks/{param1}"]
blockSsz, ok := blockData.prysmResps["ssz"].([]byte)
if !ok {
return errSszCast
}
blindedBlockData := requests["/beacon/blinded_blocks/{param1}"]
blindedBlockSsz, ok := blindedBlockData.prysmResps["ssz"].([]byte)
if !ok {
return errSszCast
}
if finalizedEpoch < helpers.AltairE2EForkEpoch+2 {
b := &ethpb.SignedBeaconBlock{}
if err := b.UnmarshalSSZ(blockSsz); err != nil {
return errors.Wrap(err, "failed to unmarshal ssz")
}
bb := &ethpb.SignedBeaconBlock{}
if err := bb.UnmarshalSSZ(blindedBlockSsz); err != nil {
return errors.Wrap(err, "failed to unmarshal ssz")
}
} else if finalizedEpoch >= helpers.AltairE2EForkEpoch+2 && finalizedEpoch < helpers.BellatrixE2EForkEpoch {
b := &ethpb.SignedBeaconBlockAltair{}
if err := b.UnmarshalSSZ(blockSsz); err != nil {
return errors.Wrap(err, "failed to unmarshal ssz")
}
bb := &ethpb.SignedBeaconBlockAltair{}
if err := bb.UnmarshalSSZ(blindedBlockSsz); err != nil {
return errors.Wrap(err, "failed to unmarshal ssz")
}
} else if finalizedEpoch >= helpers.BellatrixE2EForkEpoch && finalizedEpoch < helpers.CapellaE2EForkEpoch {
b := &ethpb.SignedBeaconBlockBellatrix{}
if err := b.UnmarshalSSZ(blockSsz); err != nil {
return errors.Wrap(err, "failed to unmarshal ssz")
}
bb := &ethpb.SignedBlindedBeaconBlockBellatrix{}
if err := bb.UnmarshalSSZ(blindedBlockSsz); err != nil {
return errors.Wrap(err, "failed to unmarshal ssz")
}
} else if finalizedEpoch >= helpers.CapellaE2EForkEpoch && finalizedEpoch < helpers.DenebE2EForkEpoch {
b := &ethpb.SignedBeaconBlockCapella{}
if err := b.UnmarshalSSZ(blockSsz); err != nil {
return errors.Wrap(err, "failed to unmarshal ssz")
}
bb := &ethpb.SignedBlindedBeaconBlockCapella{}
if err := bb.UnmarshalSSZ(blindedBlockSsz); err != nil {
return errors.Wrap(err, "failed to unmarshal ssz")
}
} else {
b := &ethpb.SignedBeaconBlockDeneb{}
if err := b.UnmarshalSSZ(blockSsz); err != nil {
return errors.Wrap(err, "failed to unmarshal ssz")
}
bb := &ethpb.SignedBlindedBeaconBlockDeneb{}
if err := bb.UnmarshalSSZ(blindedBlockSsz); err != nil {
return errors.Wrap(err, "failed to unmarshal ssz")
}
}
// verify that dependent root of proposer duties matches block header
blockHeaderData := requests["/beacon/headers/{param1}"]
header, ok := blockHeaderData.prysmResps["json"].(*beacon.GetBlockHeaderResponse)
if !ok {
return errJsonCast
}
dutiesData := requests["/validator/duties/proposer/{param1}"]
duties, ok := dutiesData.prysmResps["json"].(*validator.GetProposerDutiesResponse)
if !ok {
return errJsonCast
}
if header.Data.Root != duties.DependentRoot {
return fmt.Errorf("header root %s does not match duties root %s ", header.Data.Root, duties.DependentRoot)
}
return nil
}
func compareJSONMulticlient(
beaconNodeIdx int,
base string,
path string,
requestObj, respJSONPrysm, respJSONLighthouse interface{},
customEvaluator func(interface{}, interface{}) error,
) error {
if requestObj != nil {
if err := doJSONPostRequest(
base,
path,
beaconNodeIdx,
requestObj,
respJSONPrysm,
); err != nil {
return errors.Wrapf(err, "could not perform Prysm JSON POST request for path %s", path)
}
if err := doJSONPostRequest(
base,
path,
beaconNodeIdx,
requestObj,
respJSONLighthouse,
"lighthouse",
); err != nil {
return errors.Wrapf(err, "could not perform Lighthouse JSON POST request for path %s", path)
}
} else {
if err := doJSONGetRequest(
base,
path,
beaconNodeIdx,
respJSONPrysm,
); err != nil {
return errors.Wrapf(err, "could not perform Prysm JSON GET request for path %s", path)
}
if err := doJSONGetRequest(
base,
path,
beaconNodeIdx,
respJSONLighthouse,
"lighthouse",
); err != nil {
return errors.Wrapf(err, "could not perform Lighthouse JSON GET request for path %s", path)
}
}
if customEvaluator != nil {
return customEvaluator(respJSONPrysm, respJSONLighthouse)
} else {
return compareJSONResponseObjects(respJSONPrysm, respJSONLighthouse)
}
}
func compareSSZMulticlient(beaconNodeIdx int, base string, path string) ([]byte, []byte, error) {
sszrspL, err := doSSZGetRequest(
base,
path,
beaconNodeIdx,
"lighthouse",
)
if err != nil {
return nil, nil, errors.Wrap(err, "could not perform GET request for Lighthouse SSZ")
}
sszrspP, err := doSSZGetRequest(
base,
path,
beaconNodeIdx,
)
if err != nil {
return nil, nil, errors.Wrap(err, "could not perform GET request for Prysm SSZ")
}
if !bytes.Equal(sszrspL, sszrspP) {
return nil, nil, errors.New("prysm ssz response does not match lighthouse ssz response")
}
return sszrspP, sszrspL, nil
}
func compareJSONResponseObjects(prysmResp interface{}, lighthouseResp interface{}) error {
if !reflect.DeepEqual(prysmResp, lighthouseResp) {
p, err := json.Marshal(prysmResp)
if err != nil {
return errors.Wrap(err, "failed to marshal Prysm response to JSON")
}
l, err := json.Marshal(lighthouseResp)
if err != nil {
return errors.Wrap(err, "failed to marshal Lighthouse response to JSON")
}
return fmt.Errorf("prysm response %s does not match lighthouse response %s",
string(p),
string(l))
}
return nil
}
func pathFromParams(path string, params []string) string {
apiPath := path
for index := range params {
apiPath = strings.Replace(apiPath, fmt.Sprintf("{param%d}", index+1), params[index], 1)
}
return apiPath
}

View File

@@ -1,47 +0,0 @@
package beaconapi_evaluators
import (
"github.com/prysmaticlabs/prysm/v4/testing/endtoend/policies"
e2etypes "github.com/prysmaticlabs/prysm/v4/testing/endtoend/types"
"google.golang.org/grpc"
)
// BeaconAPIMultiClientVerifyIntegrity tests Beacon API endpoints.
// It compares responses from Prysm and other beacon nodes such as Lighthouse.
// The evaluator is executed on every odd-numbered epoch.
var BeaconAPIMultiClientVerifyIntegrity = e2etypes.Evaluator{
Name: "beacon_api_multi-client_verify_integrity_epoch_%d",
Policy: policies.EveryNEpochs(1, 2),
Evaluation: beaconAPIVerify,
}
const (
v1PathTemplate = "http://localhost:%d/eth/v1"
v2PathTemplate = "http://localhost:%d/eth/v2"
)
type apiComparisonFunc func(beaconNodeIdx int) error
func beaconAPIVerify(_ *e2etypes.EvaluationContext, conns ...*grpc.ClientConn) error {
beacon := []apiComparisonFunc{
withCompareBeaconAPIs,
}
for beaconNodeIdx := range conns {
if err := runAPIComparisonFunctions(
beaconNodeIdx,
beacon...,
); err != nil {
return err
}
}
return nil
}
func runAPIComparisonFunctions(beaconNodeIdx int, fs ...apiComparisonFunc) error {
for _, f := range fs {
if err := f(beaconNodeIdx); err != nil {
return err
}
}
return nil
}