Better Beacon API evaluator part 1 (#13084)

* Better Beacon API evaluator part 1

* rename package

* more endpoints

* rename package back

* more endpoints

* small improvements

* remove the need for `params`

---------

Co-authored-by: Nishant Das <nishdas93@gmail.com>
This commit is contained in:
Radosław Kapka
2023-10-27 13:57:49 +02:00
committed by GitHub
parent 203dc5f63b
commit 022ee17af9
6 changed files with 657 additions and 261 deletions

View File

@@ -448,7 +448,7 @@ func (s *Service) Start() {
s.cfg.Router.HandleFunc("/eth/v2/beacon/blinded_blocks", beaconChainServerV1.PublishBlindedBlockV2).Methods(http.MethodPost) s.cfg.Router.HandleFunc("/eth/v2/beacon/blinded_blocks", beaconChainServerV1.PublishBlindedBlockV2).Methods(http.MethodPost)
s.cfg.Router.HandleFunc("/eth/v1/beacon/blocks/{block_id}", beaconChainServerV1.GetBlock).Methods(http.MethodGet) s.cfg.Router.HandleFunc("/eth/v1/beacon/blocks/{block_id}", beaconChainServerV1.GetBlock).Methods(http.MethodGet)
s.cfg.Router.HandleFunc("/eth/v2/beacon/blocks/{block_id}", beaconChainServerV1.GetBlockV2).Methods(http.MethodGet) s.cfg.Router.HandleFunc("/eth/v2/beacon/blocks/{block_id}", beaconChainServerV1.GetBlockV2).Methods(http.MethodGet)
s.cfg.Router.HandleFunc("/eth/v2/beacon/blocks/{block_id}/attestations", beaconChainServerV1.GetBlockAttestations).Methods(http.MethodGet) s.cfg.Router.HandleFunc("/eth/v1/beacon/blocks/{block_id}/attestations", beaconChainServerV1.GetBlockAttestations).Methods(http.MethodGet)
s.cfg.Router.HandleFunc("/eth/v1/beacon/blinded_blocks/{block_id}", beaconChainServerV1.GetBlindedBlock).Methods(http.MethodGet) s.cfg.Router.HandleFunc("/eth/v1/beacon/blinded_blocks/{block_id}", beaconChainServerV1.GetBlindedBlock).Methods(http.MethodGet)
s.cfg.Router.HandleFunc("/eth/v1/beacon/blocks/{block_id}/root", beaconChainServerV1.GetBlockRoot).Methods(http.MethodGet) s.cfg.Router.HandleFunc("/eth/v1/beacon/blocks/{block_id}/root", beaconChainServerV1.GetBlockRoot).Methods(http.MethodGet)
s.cfg.Router.HandleFunc("/eth/v1/beacon/pool/attestations", beaconChainServerV1.ListAttestations).Methods(http.MethodGet) s.cfg.Router.HandleFunc("/eth/v1/beacon/pool/attestations", beaconChainServerV1.ListAttestations).Methods(http.MethodGet)

View File

@@ -11,6 +11,7 @@ go_library(
importpath = "github.com/prysmaticlabs/prysm/v4/testing/endtoend/evaluators/beaconapi_evaluators", importpath = "github.com/prysmaticlabs/prysm/v4/testing/endtoend/evaluators/beaconapi_evaluators",
visibility = ["//testing/endtoend:__subpackages__"], visibility = ["//testing/endtoend:__subpackages__"],
deps = [ deps = [
"//beacon-chain/rpc/apimiddleware:go_default_library",
"//beacon-chain/rpc/eth/beacon:go_default_library", "//beacon-chain/rpc/eth/beacon:go_default_library",
"//beacon-chain/rpc/eth/debug:go_default_library", "//beacon-chain/rpc/eth/debug:go_default_library",
"//beacon-chain/rpc/eth/node:go_default_library", "//beacon-chain/rpc/eth/node:go_default_library",
@@ -23,7 +24,6 @@ go_library(
"//testing/endtoend/policies:go_default_library", "//testing/endtoend/policies:go_default_library",
"//testing/endtoend/types:go_default_library", "//testing/endtoend/types:go_default_library",
"//time/slots:go_default_library", "//time/slots:go_default_library",
"@com_github_ethereum_go_ethereum//common/hexutil:go_default_library",
"@com_github_pkg_errors//:go_default_library", "@com_github_pkg_errors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library", "@com_github_sirupsen_logrus//:go_default_library",
"@org_golang_google_grpc//:go_default_library", "@org_golang_google_grpc//:go_default_library",

View File

@@ -9,8 +9,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/pkg/errors" "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/beacon"
"github.com/prysmaticlabs/prysm/v4/beacon-chain/rpc/eth/debug" "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/node"
@@ -22,7 +22,15 @@ import (
"github.com/prysmaticlabs/prysm/v4/time/slots" "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 { type metadata struct {
start primitives.Epoch
basepath string basepath string
params func(encoding string, currentEpoch primitives.Epoch) []string params func(encoding string, currentEpoch primitives.Epoch) []string
requestObject interface{} requestObject interface{}
@@ -31,12 +39,9 @@ type metadata struct {
customEvaluation func(interface{}, interface{}) error customEvaluation func(interface{}, interface{}) error
} }
var beaconPathsAndObjects = map[string]metadata{ var requests = map[string]metadata{
"/beacon/genesis": { "/beacon/genesis": {
basepath: v1MiddlewarePathTemplate, basepath: v1PathTemplate,
params: func(_ string, _ primitives.Epoch) []string {
return []string{}
},
prysmResps: map[string]interface{}{ prysmResps: map[string]interface{}{
"json": &beacon.GetGenesisResponse{}, "json": &beacon.GetGenesisResponse{},
}, },
@@ -45,7 +50,7 @@ var beaconPathsAndObjects = map[string]metadata{
}, },
}, },
"/beacon/states/{param1}/root": { "/beacon/states/{param1}/root": {
basepath: v1MiddlewarePathTemplate, basepath: v1PathTemplate,
params: func(_ string, _ primitives.Epoch) []string { params: func(_ string, _ primitives.Epoch) []string {
return []string{"head"} return []string{"head"}
}, },
@@ -56,41 +61,8 @@ var beaconPathsAndObjects = map[string]metadata{
"json": &beacon.GetStateRootResponse{}, "json": &beacon.GetStateRootResponse{},
}, },
}, },
"/beacon/states/{param1}/finality_checkpoints": {
basepath: v1MiddlewarePathTemplate,
params: func(_ string, _ primitives.Epoch) []string {
return []string{"head"}
},
prysmResps: map[string]interface{}{
"json": &beacon.GetFinalityCheckpointsResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetFinalityCheckpointsResponse{},
},
},
"/beacon/blocks/{param1}": {
basepath: v2MiddlewarePathTemplate,
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/states/{param1}/fork": { "/beacon/states/{param1}/fork": {
basepath: v1MiddlewarePathTemplate, basepath: v1PathTemplate,
params: func(_ string, _ primitives.Epoch) []string { params: func(_ string, _ primitives.Epoch) []string {
return []string{"finalized"} return []string{"finalized"}
}, },
@@ -101,9 +73,354 @@ var beaconPathsAndObjects = map[string]metadata{
"json": &beacon.GetStateForkResponse{}, "json": &beacon.GetStateForkResponse{},
}, },
}, },
"/debug/beacon/states/{param1}": { "/beacon/states/{param1}/finality_checkpoints": {
basepath: v2MiddlewarePathTemplate, 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 { 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": &apimiddleware.AttesterSlashingsPoolResponseJson{},
},
lighthouseResps: map[string]interface{}{
"json": &apimiddleware.AttesterSlashingsPoolResponseJson{},
},
customEvaluation: func(p interface{}, l interface{}) error {
pResp, ok := p.(*apimiddleware.AttesterSlashingsPoolResponseJson)
if !ok {
return errJsonCast
}
lResp, ok := l.(*apimiddleware.AttesterSlashingsPoolResponseJson)
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": &apimiddleware.ProposerSlashingsPoolResponseJson{},
},
lighthouseResps: map[string]interface{}{
"json": &apimiddleware.ProposerSlashingsPoolResponseJson{},
},
customEvaluation: func(p interface{}, l interface{}) error {
pResp, ok := p.(*apimiddleware.ProposerSlashingsPoolResponseJson)
if !ok {
return errJsonCast
}
lResp, ok := l.(*apimiddleware.ProposerSlashingsPoolResponseJson)
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": &apimiddleware.ForkScheduleResponseJson{},
},
lighthouseResps: map[string]interface{}{
"json": &apimiddleware.ForkScheduleResponseJson{},
},
customEvaluation: func(p interface{}, l interface{}) error {
// remove all forks with far-future epoch
pSchedule, ok := p.(*apimiddleware.ForkScheduleResponseJson)
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.(*apimiddleware.ForkScheduleResponseJson)
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"} return []string{"head"}
}, },
prysmResps: map[string]interface{}{ prysmResps: map[string]interface{}{
@@ -113,8 +430,136 @@ var beaconPathsAndObjects = map[string]metadata{
"json": &debug.GetBeaconStateV2Response{}, "json": &debug.GetBeaconStateV2Response{},
}, },
}, },
"/debug/beacon/heads": {
basepath: v2PathTemplate,
prysmResps: map[string]interface{}{
"json": &apimiddleware.V2ForkChoiceHeadsResponseJson{},
},
lighthouseResps: map[string]interface{}{
"json": &apimiddleware.V2ForkChoiceHeadsResponseJson{},
},
},
"/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}": { "/validator/duties/proposer/{param1}": {
basepath: v1MiddlewarePathTemplate, basepath: v1PathTemplate,
params: func(_ string, e primitives.Epoch) []string { params: func(_ string, e primitives.Epoch) []string {
return []string{fmt.Sprintf("%v", e)} return []string{fmt.Sprintf("%v", e)}
}, },
@@ -138,7 +583,7 @@ var beaconPathsAndObjects = map[string]metadata{
}, },
}, },
"/validator/duties/attester/{param1}": { "/validator/duties/attester/{param1}": {
basepath: v1MiddlewarePathTemplate, basepath: v1PathTemplate,
params: func(_ string, e primitives.Epoch) []string { params: func(_ string, e primitives.Epoch) []string {
//ask for a future epoch to test this case //ask for a future epoch to test this case
return []string{fmt.Sprintf("%v", e+1)} return []string{fmt.Sprintf("%v", e+1)}
@@ -173,99 +618,12 @@ var beaconPathsAndObjects = map[string]metadata{
return compareJSONResponseObjects(prysmResp, castedl) return compareJSONResponseObjects(prysmResp, castedl)
}, },
}, },
"/beacon/headers/{param1}": {
basepath: v1MiddlewarePathTemplate,
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{},
},
},
// we want to test comma-separated query params
"/beacon/states/{param1}/validators?id=0,1": {
basepath: v1MiddlewarePathTemplate,
params: func(_ string, e primitives.Epoch) []string {
return []string{"head"}
},
prysmResps: map[string]interface{}{
"json": &beacon.GetValidatorsResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &beacon.GetValidatorsResponse{},
},
},
"/node/identity": {
basepath: v1MiddlewarePathTemplate,
params: func(_ string, _ primitives.Epoch) []string {
return []string{}
},
prysmResps: map[string]interface{}{
"json": &node.GetIdentityResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &node.GetIdentityResponse{},
},
customEvaluation: func(prysmResp interface{}, lhouseResp interface{}) error {
castedp, ok := prysmResp.(*node.GetIdentityResponse)
if !ok {
return errors.New("failed to cast type")
}
castedl, ok := lhouseResp.(*node.GetIdentityResponse)
if !ok {
return errors.New("failed to cast type")
}
if castedp.Data == nil {
return errors.New("prysm node identity was empty")
}
if castedl.Data == nil {
return errors.New("lighthouse node identity was empty")
}
return nil
},
},
"/node/peers": {
basepath: v1MiddlewarePathTemplate,
params: func(_ string, _ primitives.Epoch) []string {
return []string{}
},
prysmResps: map[string]interface{}{
"json": &node.GetPeersResponse{},
},
lighthouseResps: map[string]interface{}{
"json": &node.GetPeersResponse{},
},
customEvaluation: func(prysmResp interface{}, lhouseResp interface{}) error {
castedp, ok := prysmResp.(*node.GetPeersResponse)
if !ok {
return errors.New("failed to cast type")
}
castedl, ok := lhouseResp.(*node.GetPeersResponse)
if !ok {
return errors.New("failed to cast type")
}
if castedp.Data == nil {
return errors.New("prysm node identity was empty")
}
if castedl.Data == nil {
return errors.New("lighthouse node identity was empty")
}
return nil
},
},
} }
func withCompareBeaconAPIs(beaconNodeIdx int) error { func withCompareBeaconAPIs(beaconNodeIdx int) error {
genesisResp := &beacon.GetGenesisResponse{} genesisResp := &beacon.GetGenesisResponse{}
err := doMiddlewareJSONGetRequest( err := doJSONGetRequest(
v1MiddlewarePathTemplate, v1PathTemplate,
"/beacon/genesis", "/beacon/genesis",
beaconNodeIdx, beaconNodeIdx,
genesisResp, genesisResp,
@@ -279,151 +637,173 @@ func withCompareBeaconAPIs(beaconNodeIdx int) error {
} }
currentEpoch := slots.EpochsSinceGenesis(time.Unix(genesisTime, 0)) currentEpoch := slots.EpochsSinceGenesis(time.Unix(genesisTime, 0))
for path, meta := range beaconPathsAndObjects { for path, meta := range requests {
if currentEpoch < meta.start {
continue
}
for key := range meta.prysmResps { for key := range meta.prysmResps {
switch key { switch key {
case "json": case "json":
jsonparams := meta.params("json", currentEpoch) apipath := path
apipath := pathFromParams(path, jsonparams) if meta.params != nil {
jsonparams := meta.params("json", currentEpoch)
apipath = pathFromParams(path, jsonparams)
}
fmt.Printf("executing json api path: %s\n", apipath) fmt.Printf("executing json api path: %s\n", apipath)
if err := compareJSONMulticlient(beaconNodeIdx, if err := compareJSONMulticlient(beaconNodeIdx,
meta.basepath, meta.basepath,
apipath, apipath,
meta.requestObject, meta.requestObject,
beaconPathsAndObjects[path].prysmResps[key], requests[path].prysmResps[key],
beaconPathsAndObjects[path].lighthouseResps[key], requests[path].lighthouseResps[key],
meta.customEvaluation, meta.customEvaluation,
); err != nil { ); err != nil {
return err return err
} }
case "ssz": case "ssz":
sszparams := meta.params("ssz", currentEpoch) apipath := path
if len(sszparams) == 0 { if meta.params != nil {
continue sszparams := meta.params("ssz", currentEpoch)
apipath = pathFromParams(path, sszparams)
} }
apipath := pathFromParams(path, sszparams)
fmt.Printf("executing ssz api path: %s\n", apipath) fmt.Printf("executing ssz api path: %s\n", apipath)
prysmr, lighthouser, err := compareSSZMulticlient(beaconNodeIdx, meta.basepath, apipath) prysmr, lighthouser, err := compareSSZMulticlient(beaconNodeIdx, meta.basepath, apipath)
if err != nil { if err != nil {
return err return err
} }
beaconPathsAndObjects[path].prysmResps[key] = prysmr requests[path].prysmResps[key] = prysmr
beaconPathsAndObjects[path].lighthouseResps[key] = lighthouser requests[path].lighthouseResps[key] = lighthouser
default: default:
return fmt.Errorf("unknown encoding type %s", key) return fmt.Errorf("unknown encoding type %s", key)
} }
} }
} }
return orderedEvaluationOnResponses(beaconPathsAndObjects, genesisResp) return postEvaluation(beaconNodeIdx, requests)
} }
func orderedEvaluationOnResponses(beaconPathsAndObjects map[string]metadata, genesisData *beacon.GetGenesisResponse) error { // postEvaluation performs additional evaluation after all requests have been completed.
forkPathData := beaconPathsAndObjects["/beacon/states/{param1}/fork"] // It is useful for things such as checking if specific fields match between endpoints.
prysmForkData, ok := forkPathData.prysmResps["json"].(*beacon.GetStateForkResponse) 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 { if !ok {
return errors.New("failed to cast type") return errJsonCast
} }
lighthouseForkData, ok := forkPathData.lighthouseResps["json"].(*beacon.GetStateForkResponse) finalizedEpoch, err := strconv.ParseUint(fork.Data.Epoch, 10, 64)
if !ok {
return errors.New("failed to cast type")
}
if prysmForkData.Data.Epoch != lighthouseForkData.Data.Epoch {
return fmt.Errorf("prysm epoch %v does not match lighthouse epoch %v",
prysmForkData.Data.Epoch,
lighthouseForkData.Data.Epoch)
}
finalizedEpoch, err := strconv.ParseUint(prysmForkData.Data.Epoch, 10, 64)
if err != nil { if err != nil {
return err return err
} }
blockPathData := beaconPathsAndObjects["/beacon/blocks/{param1}"] blockData := requests["/beacon/blocks/{param1}"]
sszrspL, ok := blockPathData.prysmResps["ssz"].([]byte) blockSsz, ok := blockData.prysmResps["ssz"].([]byte)
if !ok { if !ok {
return errors.New("failed to cast type") return errSszCast
} }
sszrspP, ok := blockPathData.lighthouseResps["ssz"].([]byte) blindedBlockData := requests["/beacon/blinded_blocks/{param1}"]
blindedBlockSsz, ok := blindedBlockData.prysmResps["ssz"].([]byte)
if !ok { if !ok {
return errors.New("failed to cast type") return errSszCast
} }
if finalizedEpoch < helpers.AltairE2EForkEpoch+2 { if finalizedEpoch < helpers.AltairE2EForkEpoch+2 {
blockP := &ethpb.SignedBeaconBlock{} b := &ethpb.SignedBeaconBlock{}
blockL := &ethpb.SignedBeaconBlock{} if err := b.UnmarshalSSZ(blockSsz); err != nil {
if err := blockL.UnmarshalSSZ(sszrspL); err != nil { return errors.Wrap(err, "failed to unmarshal ssz")
return errors.Wrap(err, "failed to unmarshal lighthouse ssz")
} }
if err := blockP.UnmarshalSSZ(sszrspP); err != nil { bb := &ethpb.SignedBeaconBlock{}
return errors.Wrap(err, "failed to unmarshal rysm ssz") if err := bb.UnmarshalSSZ(blindedBlockSsz); err != nil {
} return errors.Wrap(err, "failed to unmarshal ssz")
if len(blockP.Signature) == 0 || len(blockL.Signature) == 0 || hexutil.Encode(blockP.Signature) != hexutil.Encode(blockL.Signature) {
return errors.New("prysm signature does not match lighthouse signature")
} }
} else if finalizedEpoch >= helpers.AltairE2EForkEpoch+2 && finalizedEpoch < helpers.BellatrixE2EForkEpoch { } else if finalizedEpoch >= helpers.AltairE2EForkEpoch+2 && finalizedEpoch < helpers.BellatrixE2EForkEpoch {
blockP := &ethpb.SignedBeaconBlockAltair{} b := &ethpb.SignedBeaconBlockAltair{}
blockL := &ethpb.SignedBeaconBlockAltair{} if err := b.UnmarshalSSZ(blockSsz); err != nil {
if err := blockL.UnmarshalSSZ(sszrspL); err != nil { return errors.Wrap(err, "failed to unmarshal ssz")
return errors.Wrap(err, "lighthouse ssz error")
} }
if err := blockP.UnmarshalSSZ(sszrspP); err != nil { bb := &ethpb.SignedBeaconBlockAltair{}
return errors.Wrap(err, "prysm ssz error") if err := bb.UnmarshalSSZ(blindedBlockSsz); err != nil {
return errors.Wrap(err, "failed to unmarshal ssz")
} }
} else if finalizedEpoch >= helpers.BellatrixE2EForkEpoch && finalizedEpoch < helpers.CapellaE2EForkEpoch {
if len(blockP.Signature) == 0 || len(blockL.Signature) == 0 || hexutil.Encode(blockP.Signature) != hexutil.Encode(blockL.Signature) { b := &ethpb.SignedBeaconBlockBellatrix{}
return fmt.Errorf("prysm response %v does not match lighthouse response %v", if err := b.UnmarshalSSZ(blockSsz); err != nil {
blockP, return errors.Wrap(err, "failed to unmarshal ssz")
blockL) }
bb := &ethpb.SignedBlindedBeaconBlockBellatrix{}
if err := bb.UnmarshalSSZ(blindedBlockSsz); err != nil {
return errors.Wrap(err, "failed to unmarshal ssz")
} }
} else { } else {
blockP := &ethpb.SignedBeaconBlockBellatrix{} b := &ethpb.SignedBeaconBlockCapella{}
blockL := &ethpb.SignedBeaconBlockBellatrix{} if err := b.UnmarshalSSZ(blockSsz); err != nil {
if err := blockL.UnmarshalSSZ(sszrspL); err != nil { return errors.Wrap(err, "failed to unmarshal ssz")
return errors.Wrap(err, "lighthouse ssz error")
} }
if err := blockP.UnmarshalSSZ(sszrspP); err != nil { bb := &ethpb.SignedBlindedBeaconBlockCapella{}
return errors.Wrap(err, "prysm ssz error") if err := bb.UnmarshalSSZ(blindedBlockSsz); err != nil {
return errors.Wrap(err, "failed to unmarshal ssz")
} }
}
if len(blockP.Signature) == 0 || len(blockL.Signature) == 0 || hexutil.Encode(blockP.Signature) != hexutil.Encode(blockL.Signature) { // verify that dependent root of proposer duties matches block header
return fmt.Errorf("prysm response %v does not match lighthouse response %v", blockHeaderData := requests["/beacon/headers/{param1}"]
blockP, header, ok := blockHeaderData.prysmResps["json"].(*beacon.GetBlockHeaderResponse)
blockL)
}
}
blockheaderData := beaconPathsAndObjects["/beacon/headers/{param1}"]
prysmHeader, ok := blockheaderData.prysmResps["json"].(*beacon.GetBlockHeaderResponse)
if !ok { if !ok {
return errors.New("failed to cast type") return errJsonCast
} }
proposerdutiesData := beaconPathsAndObjects["/validator/duties/proposer/{param1}"] dutiesData := requests["/validator/duties/proposer/{param1}"]
prysmDuties, ok := proposerdutiesData.prysmResps["json"].(*validator.GetProposerDutiesResponse) duties, ok := dutiesData.prysmResps["json"].(*validator.GetProposerDutiesResponse)
if !ok { if !ok {
return errors.New("failed to cast type") return errJsonCast
} }
if prysmHeader.Data.Root != prysmDuties.DependentRoot { if header.Data.Root != duties.DependentRoot {
genesisTime, err := strconv.ParseUint(genesisData.Data.GenesisTime, 10, 64) return fmt.Errorf("header root %s does not match duties root %s ", header.Data.Root, duties.DependentRoot)
if err != nil { }
return errors.Wrapf(err, "could not parse genesis time")
} // get first peer returned from /node/peers and use it's ID to test the /node/peers/{peer_id} endpoint
fmt.Printf("current slot: %v\n", slots.CurrentSlot(genesisTime)) peersData := requests["/node/peers"]
return fmt.Errorf("header root %s does not match duties root %s ", prysmHeader.Data.Root, prysmDuties.DependentRoot) pPeers, ok := peersData.prysmResps["json"].(*node.GetPeersResponse)
if !ok {
return errJsonCast
}
pPeer := &node.GetPeerResponse{}
if err = doJSONGetRequest(v1PathTemplate, "/node/peers/"+pPeers.Data[0].PeerId, beaconNodeIdx, pPeer); err != nil {
return err
}
if pPeer.Data == nil {
return errEmptyPrysmData
}
lPeers, ok := peersData.lighthouseResps["json"].(*node.GetPeersResponse)
if !ok {
return errJsonCast
}
lPeer := &node.GetPeerResponse{}
if err = doJSONGetRequest(v1PathTemplate, "/node/peers/"+lPeers.Data[0].PeerId, beaconNodeIdx, lPeer, "lighthouse"); err != nil {
return err
}
if lPeer.Data == nil {
return errEmptyLighthouseData
} }
return nil return nil
} }
func compareJSONMulticlient(beaconNodeIdx int, base string, path string, requestObj, respJSONPrysm interface{}, respJSONLighthouse interface{}, customEvaluator func(interface{}, interface{}) error) error { func compareJSONMulticlient(
beaconNodeIdx int,
base string,
path string,
requestObj, respJSONPrysm, respJSONLighthouse interface{},
customEvaluator func(interface{}, interface{}) error,
) error {
if requestObj != nil { if requestObj != nil {
if err := doMiddlewareJSONPostRequest( if err := doJSONPostRequest(
base, base,
path, path,
beaconNodeIdx, beaconNodeIdx,
requestObj, requestObj,
respJSONPrysm, respJSONPrysm,
); err != nil { ); err != nil {
return errors.Wrap(err, "could not perform POST request for Prysm JSON") return errors.Wrapf(err, "could not perform Prysm JSON POST request for path %s", path)
} }
if err := doMiddlewareJSONPostRequest( if err := doJSONPostRequest(
base, base,
path, path,
beaconNodeIdx, beaconNodeIdx,
@@ -431,26 +811,26 @@ func compareJSONMulticlient(beaconNodeIdx int, base string, path string, request
respJSONLighthouse, respJSONLighthouse,
"lighthouse", "lighthouse",
); err != nil { ); err != nil {
return errors.Wrap(err, "could not perform POST request for Lighthouse JSON") return errors.Wrapf(err, "could not perform Lighthouse JSON POST request for path %s", path)
} }
} else { } else {
if err := doMiddlewareJSONGetRequest( if err := doJSONGetRequest(
base, base,
path, path,
beaconNodeIdx, beaconNodeIdx,
respJSONPrysm, respJSONPrysm,
); err != nil { ); err != nil {
return errors.Wrap(err, "could not perform GET request for Prysm JSON") return errors.Wrapf(err, "could not perform Prysm JSON GET request for path %s", path)
} }
if err := doMiddlewareJSONGetRequest( if err := doJSONGetRequest(
base, base,
path, path,
beaconNodeIdx, beaconNodeIdx,
respJSONLighthouse, respJSONLighthouse,
"lighthouse", "lighthouse",
); err != nil { ); err != nil {
return errors.Wrap(err, "could not perform GET request for Lighthouse JSON") return errors.Wrapf(err, "could not perform Lighthouse JSON GET request for path %s", path)
} }
} }
if customEvaluator != nil { if customEvaluator != nil {
@@ -461,7 +841,7 @@ func compareJSONMulticlient(beaconNodeIdx int, base string, path string, request
} }
func compareSSZMulticlient(beaconNodeIdx int, base string, path string) ([]byte, []byte, error) { func compareSSZMulticlient(beaconNodeIdx int, base string, path string) ([]byte, []byte, error) {
sszrspL, err := doMiddlewareSSZGetRequest( sszrspL, err := doSSZGetRequest(
base, base,
path, path,
beaconNodeIdx, beaconNodeIdx,
@@ -471,7 +851,7 @@ func compareSSZMulticlient(beaconNodeIdx int, base string, path string) ([]byte,
return nil, nil, errors.Wrap(err, "could not perform GET request for Lighthouse SSZ") return nil, nil, errors.Wrap(err, "could not perform GET request for Lighthouse SSZ")
} }
sszrspP, err := doMiddlewareSSZGetRequest( sszrspP, err := doSSZGetRequest(
base, base,
path, path,
beaconNodeIdx, beaconNodeIdx,
@@ -505,7 +885,7 @@ func compareJSONResponseObjects(prysmResp interface{}, lighthouseResp interface{
func pathFromParams(path string, params []string) string { func pathFromParams(path string, params []string) string {
apiPath := path apiPath := path
for index := range params { for index := range params {
apiPath = strings.Replace(path, fmt.Sprintf("{param%d}", index+1), params[index], 1) apiPath = strings.Replace(apiPath, fmt.Sprintf("{param%d}", index+1), params[index], 1)
} }
return apiPath return apiPath
} }

View File

@@ -6,16 +6,18 @@ import (
"google.golang.org/grpc" "google.golang.org/grpc"
) )
// BeaconAPIMultiClientVerifyIntegrity tests our API Middleware responses to other beacon nodes such as lighthouse. // 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{ var BeaconAPIMultiClientVerifyIntegrity = e2etypes.Evaluator{
Name: "beacon_api_multi-client_verify_integrity_epoch_%d", Name: "beacon_api_multi-client_verify_integrity_epoch_%d",
Policy: policies.AfterNthEpoch(0), Policy: policies.EveryNEpochs(1, 2),
Evaluation: beaconAPIVerify, Evaluation: beaconAPIVerify,
} }
const ( const (
v1MiddlewarePathTemplate = "http://localhost:%d/eth/v1" v1PathTemplate = "http://localhost:%d/eth/v1"
v2MiddlewarePathTemplate = "http://localhost:%d/eth/v2" v2PathTemplate = "http://localhost:%d/eth/v2"
) )
type apiComparisonFunc func(beaconNodeIdx int) error type apiComparisonFunc func(beaconNodeIdx int) error

View File

@@ -11,17 +11,19 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
func doMiddlewareJSONGetRequest(template string, requestPath string, beaconNodeIdx int, dst interface{}, bnType ...string) error { func doJSONGetRequest(template string, requestPath string, beaconNodeIdx int, dst interface{}, bnType ...string) error {
if len(bnType) == 0 {
bnType = []string{"prysm"}
}
var port int var port int
if len(bnType) > 0 { switch bnType[0] {
switch bnType[0] { case "prysm":
case "lighthouse":
port = params.TestParams.Ports.LighthouseBeaconNodeHTTPPort
default:
port = params.TestParams.Ports.PrysmBeaconNodeGatewayPort
}
} else {
port = params.TestParams.Ports.PrysmBeaconNodeGatewayPort port = params.TestParams.Ports.PrysmBeaconNodeGatewayPort
case "lighthouse":
port = params.TestParams.Ports.LighthouseBeaconNodeHTTPPort
default:
return fmt.Errorf("unknown node type %s", bnType[0])
} }
basePath := fmt.Sprintf(template, port+beaconNodeIdx) basePath := fmt.Sprintf(template, port+beaconNodeIdx)
@@ -36,23 +38,25 @@ func doMiddlewareJSONGetRequest(template string, requestPath string, beaconNodeI
if err := json.NewDecoder(httpResp.Body).Decode(&body); err != nil { if err := json.NewDecoder(httpResp.Body).Decode(&body); err != nil {
return err return err
} }
return fmt.Errorf("request failed with response code: %d with response body %s", httpResp.StatusCode, body) return fmt.Errorf("%s request failed with response code: %d with response body %s", bnType[0], httpResp.StatusCode, body)
} }
return json.NewDecoder(httpResp.Body).Decode(&dst) return json.NewDecoder(httpResp.Body).Decode(&dst)
} }
func doMiddlewareSSZGetRequest(template string, requestPath string, beaconNodeIdx int, bnType ...string) ([]byte, error) { func doSSZGetRequest(template string, requestPath string, beaconNodeIdx int, bnType ...string) ([]byte, error) {
if len(bnType) == 0 {
bnType = []string{"prysm"}
}
client := &http.Client{} client := &http.Client{}
var port int var port int
if len(bnType) > 0 { switch bnType[0] {
switch bnType[0] { case "prysm":
case "lighthouse":
port = params.TestParams.Ports.LighthouseBeaconNodeHTTPPort
default:
port = params.TestParams.Ports.PrysmBeaconNodeGatewayPort
}
} else {
port = params.TestParams.Ports.PrysmBeaconNodeGatewayPort port = params.TestParams.Ports.PrysmBeaconNodeGatewayPort
case "lighthouse":
port = params.TestParams.Ports.LighthouseBeaconNodeHTTPPort
default:
return nil, fmt.Errorf("unknown node type %s", bnType[0])
} }
basePath := fmt.Sprintf(template, port+beaconNodeIdx) basePath := fmt.Sprintf(template, port+beaconNodeIdx)
@@ -71,7 +75,7 @@ func doMiddlewareSSZGetRequest(template string, requestPath string, beaconNodeId
if err := json.NewDecoder(rsp.Body).Decode(&body); err != nil { if err := json.NewDecoder(rsp.Body).Decode(&body); err != nil {
return nil, err return nil, err
} }
return nil, fmt.Errorf("request failed with response code: %d with response body %s", rsp.StatusCode, body) return nil, fmt.Errorf("%s request failed with response code: %d with response body %s", bnType[0], rsp.StatusCode, body)
} }
defer closeBody(rsp.Body) defer closeBody(rsp.Body)
body, err := io.ReadAll(rsp.Body) body, err := io.ReadAll(rsp.Body)
@@ -82,18 +86,21 @@ func doMiddlewareSSZGetRequest(template string, requestPath string, beaconNodeId
return body, nil return body, nil
} }
func doMiddlewareJSONPostRequest(template string, requestPath string, beaconNodeIdx int, postData, dst interface{}, bnType ...string) error { func doJSONPostRequest(template string, requestPath string, beaconNodeIdx int, postData, dst interface{}, bnType ...string) error {
var port int if len(bnType) == 0 {
if len(bnType) > 0 { bnType = []string{"prysm"}
switch bnType[0] {
case "lighthouse":
port = params.TestParams.Ports.LighthouseBeaconNodeHTTPPort
default:
port = params.TestParams.Ports.PrysmBeaconNodeGatewayPort
}
} else {
port = params.TestParams.Ports.PrysmBeaconNodeGatewayPort
} }
var port int
switch bnType[0] {
case "prysm":
port = params.TestParams.Ports.PrysmBeaconNodeGatewayPort
case "lighthouse":
port = params.TestParams.Ports.LighthouseBeaconNodeHTTPPort
default:
return fmt.Errorf("unknown node type %s", bnType[0])
}
basePath := fmt.Sprintf(template, port+beaconNodeIdx) basePath := fmt.Sprintf(template, port+beaconNodeIdx)
b, err := json.Marshal(postData) b, err := json.Marshal(postData)
if err != nil { if err != nil {
@@ -112,7 +119,7 @@ func doMiddlewareJSONPostRequest(template string, requestPath string, beaconNode
if err := json.NewDecoder(httpResp.Body).Decode(&body); err != nil { if err := json.NewDecoder(httpResp.Body).Decode(&body); err != nil {
return err return err
} }
return fmt.Errorf("request failed with response code: %d with response body %s", httpResp.StatusCode, body) return fmt.Errorf("%s request failed with response code: %d with response body %s", bnType[0], httpResp.StatusCode, body)
} }
return json.NewDecoder(httpResp.Body).Decode(&dst) return json.NewDecoder(httpResp.Body).Decode(&dst)
} }

View File

@@ -34,3 +34,10 @@ func BetweenEpochs(fromEpoch, toEpoch primitives.Epoch) func(primitives.Epoch) b
return fromEpoch < currentEpoch && currentEpoch < toEpoch return fromEpoch < currentEpoch && currentEpoch < toEpoch
} }
} }
// EveryNEpochs runs every N epochs, starting with the provided epoch.
func EveryNEpochs(onwardsEpoch primitives.Epoch, n primitives.Epoch) func(epoch primitives.Epoch) bool {
return func(currentEpoch primitives.Epoch) bool {
return currentEpoch >= onwardsEpoch && ((currentEpoch-onwardsEpoch)%n == 0)
}
}