From 804f2806ce343e651c231f18221a9590acfb7aae Mon Sep 17 00:00:00 2001 From: Dhruv Bodani Date: Thu, 13 Oct 2022 15:59:14 +0530 Subject: [PATCH 1/3] add sync comm duties to vapi --- core/validatorapi/eth2types.go | 38 +++++++++++++++++++++++ core/validatorapi/router.go | 32 +++++++++++++++++++ core/validatorapi/router_internal_test.go | 37 ++++++++++++++++++++++ core/validatorapi/validatorapi.go | 18 +++++++++++ 4 files changed, 125 insertions(+) diff --git a/core/validatorapi/eth2types.go b/core/validatorapi/eth2types.go index aed696f58..1e6df0d4e 100644 --- a/core/validatorapi/eth2types.go +++ b/core/validatorapi/eth2types.go @@ -135,3 +135,41 @@ func (v v1Validator) MarshalJSON() ([]byte, error) { return bytes.ToLower(b), nil // ValidatorState must be lower case. } + +// syncCommitteeDutiesRequest defines the request to the getSyncCommitteeDuties endpoint. +// See https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/getSyncCommitteeDuties. +type syncCommitteeDutiesRequest []eth2p0.ValidatorIndex + +func (r *syncCommitteeDutiesRequest) UnmarshalJSON(bytes []byte) error { + // First try normal json number array + var ints []uint64 + if err := json.Unmarshal(bytes, &ints); err == nil { + for _, i := range ints { + *r = append(*r, eth2p0.ValidatorIndex(i)) + } + + return nil + } + + // Then try string json number array + var strints []string + if err := json.Unmarshal(bytes, &strints); err != nil { + return errors.Wrap(err, "unmarshal slice") + } + + for _, strint := range strints { + i, err := strconv.ParseUint(strint, 10, 64) + if err != nil { + return errors.Wrap(err, "parse index") + } + *r = append(*r, eth2p0.ValidatorIndex(i)) + } + + return nil +} + +// syncCommitteeDutiesResponse defines the response to the getSyncCommitteeDuties endpoint. +// See https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/getSyncCommitteeDuties. +type syncCommitteeDutiesResponse struct { + Data []*eth2v1.SyncCommitteeDuty `json:"data"` +} diff --git a/core/validatorapi/router.go b/core/validatorapi/router.go index 489321205..69021f1f3 100644 --- a/core/validatorapi/router.go +++ b/core/validatorapi/router.go @@ -66,6 +66,7 @@ type Handler interface { eth2client.BlindedBeaconBlockProposalProvider eth2client.BlindedBeaconBlockSubmitter eth2client.ProposerDutiesProvider + eth2client.SyncCommitteeDutiesProvider eth2client.SyncCommitteeMessagesSubmitter eth2client.ValidatorsProvider eth2client.ValidatorRegistrationsSubmitter @@ -94,6 +95,11 @@ func NewRouter(h Handler, eth2Cl eth2wrap.Client) (*mux.Router, error) { Path: "/eth/v1/validator/duties/proposer/{epoch}", Handler: proposerDuties(h), }, + { + Name: "sync_committee_duties", + Path: "/eth/v1/validator/duties/sync/{epoch}", + Handler: syncCommitteeDuties(h), + }, { Name: "attestation_data", Path: "/eth/v1/validator/attestation_data", @@ -384,6 +390,32 @@ func attesterDuties(p eth2client.AttesterDutiesProvider) handlerFunc { } } +// syncCommitteeDuties returns a handler function for the sync committee duty endpoint. +func syncCommitteeDuties(p eth2client.SyncCommitteeDutiesProvider) handlerFunc { + return func(ctx context.Context, params map[string]string, query url.Values, body []byte) (interface{}, error) { + epoch, err := uintParam(params, "epoch") + if err != nil { + return nil, err + } + + var req syncCommitteeDutiesRequest + if err := unmarshal(body, &req); err != nil { + return nil, err + } + + data, err := p.SyncCommitteeDuties(ctx, eth2p0.Epoch(epoch), req) + if err != nil { + return nil, err + } + + if len(data) == 0 { + data = []*eth2v1.SyncCommitteeDuty{} + } + + return syncCommitteeDutiesResponse{Data: data}, nil + } +} + // proposeBlock receives the randao from the validator and returns the unsigned BeaconBlock. func proposeBlock(p eth2client.BeaconBlockProposalProvider) handlerFunc { return func(ctx context.Context, params map[string]string, query url.Values, body []byte) (interface{}, error) { diff --git a/core/validatorapi/router_internal_test.go b/core/validatorapi/router_internal_test.go index 5239f665d..43f96171b 100644 --- a/core/validatorapi/router_internal_test.go +++ b/core/validatorapi/router_internal_test.go @@ -279,6 +279,38 @@ func TestRouter(t *testing.T) { testRouter(t, handler, callback) }) + t.Run("synccommduty", func(t *testing.T) { + handler := testHandler{ + SyncCommitteeDutiesFunc: func(ctx context.Context, epoch eth2p0.Epoch, vIdxs []eth2p0.ValidatorIndex) ([]*eth2v1.SyncCommitteeDuty, error) { + // Returns ordered total number of duties for the epoch + var res []*eth2v1.SyncCommitteeDuty + for _, vIdx := range vIdxs { + res = append(res, ð2v1.SyncCommitteeDuty{ + ValidatorIndex: vIdx, + ValidatorSyncCommitteeIndices: []eth2p0.CommitteeIndex{eth2p0.CommitteeIndex(vIdx)}, + }) + } + + return res, nil + }, + } + + callback := func(ctx context.Context, cl *eth2http.Service) { + const epoch = 4 + const validator = 1 + res, err := cl.SyncCommitteeDuties(ctx, eth2p0.Epoch(epoch), []eth2p0.ValidatorIndex{ + eth2p0.ValidatorIndex(validator), // Only request 1 of total 2 validators + }) + require.NoError(t, err) + + require.Len(t, res, 1) + require.Equal(t, res[0].ValidatorSyncCommitteeIndices, []eth2p0.CommitteeIndex{eth2p0.CommitteeIndex(validator)}) + require.Equal(t, int(res[0].ValidatorIndex), validator) + } + + testRouter(t, handler, callback) + }) + t.Run("get_validator_index", func(t *testing.T) { handler := testHandler{ ValidatorsFunc: func(_ context.Context, stateID string, indices []eth2p0.ValidatorIndex) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) { @@ -740,6 +772,7 @@ type testHandler struct { SubmitBeaconCommitteeSubscriptionsV2Func func(ctx context.Context, subscriptions []*eth2exp.BeaconCommitteeSubscription) ([]*eth2exp.BeaconCommitteeSubscriptionResponse, error) SubmitAggregateAttestationsFunc func(ctx context.Context, aggregateAndProofs []*eth2p0.SignedAggregateAndProof) error SubmitSyncCommitteeMessagesFunc func(ctx context.Context, messages []*altair.SyncCommitteeMessage) error + SyncCommitteeDutiesFunc func(ctx context.Context, epoch eth2p0.Epoch, validatorIndices []eth2p0.ValidatorIndex) ([]*eth2v1.SyncCommitteeDuty, error) } func (h testHandler) AttestationData(ctx context.Context, slot eth2p0.Slot, commIdx eth2p0.CommitteeIndex) (*eth2p0.AttestationData, error) { @@ -798,6 +831,10 @@ func (h testHandler) SubmitSyncCommitteeMessages(ctx context.Context, messages [ return h.SubmitSyncCommitteeMessagesFunc(ctx, messages) } +func (h testHandler) SyncCommitteeDuties(ctx context.Context, epoch eth2p0.Epoch, validatorIndices []eth2p0.ValidatorIndex) ([]*eth2v1.SyncCommitteeDuty, error) { + return h.SyncCommitteeDutiesFunc(ctx, epoch, validatorIndices) +} + // newBeaconHandler returns a mock beacon node handler. It registers a few mock handlers required by the // eth2http service on startup, all other requests are routed to ProxyHandler if not nil. func (h testHandler) newBeaconHandler(t *testing.T) http.Handler { diff --git a/core/validatorapi/validatorapi.go b/core/validatorapi/validatorapi.go index 9e07245fc..c6b52c61e 100644 --- a/core/validatorapi/validatorapi.go +++ b/core/validatorapi/validatorapi.go @@ -822,6 +822,24 @@ func (c Component) AttesterDuties(ctx context.Context, epoch eth2p0.Epoch, valid return duties, nil } +func (c Component) SyncCommitteeDuties(ctx context.Context, epoch eth2p0.Epoch, validatorIndices []eth2p0.ValidatorIndex) ([]*eth2v1.SyncCommitteeDuty, error) { + duties, err := c.eth2Cl.SyncCommitteeDuties(ctx, epoch, validatorIndices) + if err != nil { + return nil, err + } + + // Replace root public keys with public shares. + for i := 0; i < len(duties); i++ { + pubshare, ok := c.getPubShareFunc(duties[i].PubKey) + if !ok { + return nil, errors.New("pubshare not found") + } + duties[i].PubKey = pubshare + } + + return duties, nil +} + func (c Component) Validators(ctx context.Context, stateID string, validatorIndices []eth2p0.ValidatorIndex) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) { vals, err := c.eth2Cl.Validators(ctx, stateID, validatorIndices) if err != nil { From a9468fdfe6ee76bd7b29a6494a0b9b17640a405a Mon Sep 17 00:00:00 2001 From: Dhruv Bodani Date: Thu, 13 Oct 2022 17:36:36 +0530 Subject: [PATCH 2/3] cleanup --- testutil/beaconmock/options.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/testutil/beaconmock/options.go b/testutil/beaconmock/options.go index a8aca6e7d..d16eb6428 100644 --- a/testutil/beaconmock/options.go +++ b/testutil/beaconmock/options.go @@ -533,6 +533,9 @@ func defaultMock(httpMock HTTPMock, httpServer *http.Server, clock clockwork.Clo SubmitProposalPreparationsFunc: func(_ context.Context, _ []*eth2v1.ProposalPreparation) error { return nil }, + SyncCommitteeDutiesFunc: func(ctx context.Context, epoch eth2p0.Epoch, validatorIndices []eth2p0.ValidatorIndex) ([]*eth2v1.SyncCommitteeDuty, error) { + return []*eth2v1.SyncCommitteeDuty{}, nil + }, } } From db0403c6581ff824393d635f5f07b7fd52442595 Mon Sep 17 00:00:00 2001 From: Dhruv Bodani Date: Thu, 13 Oct 2022 18:28:46 +0530 Subject: [PATCH 3/3] cleanup --- core/validatorapi/eth2types.go | 41 ++---------- core/validatorapi/router.go | 16 +---- core/validatorapi/validatorapi_test.go | 92 +++++++++++++++++++------- 3 files changed, 76 insertions(+), 73 deletions(-) diff --git a/core/validatorapi/eth2types.go b/core/validatorapi/eth2types.go index 1e6df0d4e..425941e54 100644 --- a/core/validatorapi/eth2types.go +++ b/core/validatorapi/eth2types.go @@ -38,11 +38,12 @@ type errorResponse struct { // TODO(corver): Maybe add stacktraces field for debugging. } -// attesterDutiesRequest defines the request to the getAttesterDuties and getProposerDuties endpoint. -// See https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/getAttesterDuties. -type attesterDutiesRequest []eth2p0.ValidatorIndex +// valIndexesJSON defines the request to the getAttesterDuties and getSyncCommitteeDuties endpoint. +// See https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/getAttesterDuties and +// https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/getSyncCommitteeDuties. +type valIndexesJSON []eth2p0.ValidatorIndex -func (r *attesterDutiesRequest) UnmarshalJSON(bytes []byte) error { +func (r *valIndexesJSON) UnmarshalJSON(bytes []byte) error { // First try normal json number array var ints []uint64 if err := json.Unmarshal(bytes, &ints); err == nil { @@ -136,38 +137,6 @@ func (v v1Validator) MarshalJSON() ([]byte, error) { return bytes.ToLower(b), nil // ValidatorState must be lower case. } -// syncCommitteeDutiesRequest defines the request to the getSyncCommitteeDuties endpoint. -// See https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/getSyncCommitteeDuties. -type syncCommitteeDutiesRequest []eth2p0.ValidatorIndex - -func (r *syncCommitteeDutiesRequest) UnmarshalJSON(bytes []byte) error { - // First try normal json number array - var ints []uint64 - if err := json.Unmarshal(bytes, &ints); err == nil { - for _, i := range ints { - *r = append(*r, eth2p0.ValidatorIndex(i)) - } - - return nil - } - - // Then try string json number array - var strints []string - if err := json.Unmarshal(bytes, &strints); err != nil { - return errors.Wrap(err, "unmarshal slice") - } - - for _, strint := range strints { - i, err := strconv.ParseUint(strint, 10, 64) - if err != nil { - return errors.Wrap(err, "parse index") - } - *r = append(*r, eth2p0.ValidatorIndex(i)) - } - - return nil -} - // syncCommitteeDutiesResponse defines the response to the getSyncCommitteeDuties endpoint. // See https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/getSyncCommitteeDuties. type syncCommitteeDutiesResponse struct { diff --git a/core/validatorapi/router.go b/core/validatorapi/router.go index 69021f1f3..a7d9e624a 100644 --- a/core/validatorapi/router.go +++ b/core/validatorapi/router.go @@ -350,10 +350,6 @@ func proposerDuties(p eth2client.ProposerDutiesProvider) handlerFunc { return nil, err } - if len(data) == 0 { - data = []*eth2v1.ProposerDuty{} - } - return proposerDutiesResponse{ DependentRoot: stubRoot(epoch), // TODO(corver): Fill this properly Data: data, @@ -369,7 +365,7 @@ func attesterDuties(p eth2client.AttesterDutiesProvider) handlerFunc { return nil, err } - var req attesterDutiesRequest + var req valIndexesJSON if err := unmarshal(body, &req); err != nil { return nil, err } @@ -379,10 +375,6 @@ func attesterDuties(p eth2client.AttesterDutiesProvider) handlerFunc { return nil, err } - if len(data) == 0 { - data = []*eth2v1.AttesterDuty{} - } - return attesterDutiesResponse{ DependentRoot: stubRoot(epoch), // TODO(corver): Fill this properly Data: data, @@ -398,7 +390,7 @@ func syncCommitteeDuties(p eth2client.SyncCommitteeDutiesProvider) handlerFunc { return nil, err } - var req syncCommitteeDutiesRequest + var req valIndexesJSON if err := unmarshal(body, &req); err != nil { return nil, err } @@ -408,10 +400,6 @@ func syncCommitteeDuties(p eth2client.SyncCommitteeDutiesProvider) handlerFunc { return nil, err } - if len(data) == 0 { - data = []*eth2v1.SyncCommitteeDuty{} - } - return syncCommitteeDutiesResponse{Data: data}, nil } } diff --git a/core/validatorapi/validatorapi_test.go b/core/validatorapi/validatorapi_test.go index fc1524668..859d03d04 100644 --- a/core/validatorapi/validatorapi_test.go +++ b/core/validatorapi/validatorapi_test.go @@ -17,7 +17,6 @@ package validatorapi_test import ( "context" - "crypto/rand" "fmt" mrand "math/rand" "sync" @@ -992,43 +991,90 @@ func TestComponent_SubmitVoluntaryExitInvalidSignature(t *testing.T) { require.ErrorContains(t, err, "invalid signature") } -func TestComponent_ProposerDuties(t *testing.T) { +func TestComponent_Duties(t *testing.T) { ctx := context.Background() // Configure validator - const vIdx = 1 + const ( + vIdx = 123 + epch = 456 + ) - tss, _, err := tbls.GenerateTSS(3, 4, rand.Reader) - require.NoError(t, err) + // Create pubkey and pubshare + eth2Pubkey := testutil.RandomEth2PubKey(t) + eth2Share := testutil.RandomEth2PubKey(t) - // Create keys (just use normal keys, not split tbls) - pubkey := tss.PublicKey() - pubshare := tss.PublicShare(vIdx) - - eth2Share, err := tblsconv.KeyToETH2(pubshare) + pubshare, err := tblsconv.KeyFromETH2(eth2Share) require.NoError(t, err) - validator := beaconmock.ValidatorSetA[vIdx] - validator.Validator.PublicKey, err = tblsconv.KeyToETH2(pubkey) + pubkey, err := tblsconv.KeyFromETH2(eth2Pubkey) require.NoError(t, err) pubShareByKey := map[*bls_sig.PublicKey]*bls_sig.PublicKey{pubkey: pubshare} // Maps self to self since not tbls // Configure beacon mock - bmock, err := beaconmock.New( - beaconmock.WithValidatorSet(beaconmock.ValidatorSet{vIdx: validator}), - beaconmock.WithDeterministicProposerDuties(0), // All duties in first slot of epoch. - ) + bmock, err := beaconmock.New() require.NoError(t, err) - // Construct the validator api component - vapi, err := validatorapi.NewComponent(bmock, pubShareByKey, 0, "") - require.NoError(t, err) + t.Run("proposer_duties", func(t *testing.T) { + bmock.ProposerDutiesFunc = func(ctx context.Context, epoch eth2p0.Epoch, indices []eth2p0.ValidatorIndex) ([]*eth2v1.ProposerDuty, error) { + require.Equal(t, epoch, eth2p0.Epoch(epch)) + require.Equal(t, []eth2p0.ValidatorIndex{eth2p0.ValidatorIndex(vIdx)}, indices) - duties, err := vapi.ProposerDuties(ctx, eth2p0.Epoch(0), []eth2p0.ValidatorIndex{eth2p0.ValidatorIndex(vIdx)}) - require.NoError(t, err) - require.Len(t, duties, 1) - require.Equal(t, duties[0].PubKey, eth2Share) + return []*eth2v1.ProposerDuty{{ + PubKey: eth2Pubkey, + ValidatorIndex: vIdx, + }}, nil + } + + // Construct the validator api component + vapi, err := validatorapi.NewComponent(bmock, pubShareByKey, 0, "") + require.NoError(t, err) + duties, err := vapi.ProposerDuties(ctx, eth2p0.Epoch(epch), []eth2p0.ValidatorIndex{eth2p0.ValidatorIndex(vIdx)}) + require.NoError(t, err) + require.Len(t, duties, 1) + require.Equal(t, duties[0].PubKey, eth2Share) + }) + + t.Run("attester_duties", func(t *testing.T) { + bmock.AttesterDutiesFunc = func(_ context.Context, epoch eth2p0.Epoch, indices []eth2p0.ValidatorIndex) ([]*eth2v1.AttesterDuty, error) { + require.Equal(t, epoch, eth2p0.Epoch(epch)) + require.Equal(t, []eth2p0.ValidatorIndex{eth2p0.ValidatorIndex(vIdx)}, indices) + + return []*eth2v1.AttesterDuty{{ + PubKey: eth2Pubkey, + ValidatorIndex: vIdx, + }}, nil + } + + // Construct the validator api component + vapi, err := validatorapi.NewComponent(bmock, pubShareByKey, 0, "") + require.NoError(t, err) + duties, err := vapi.AttesterDuties(ctx, eth2p0.Epoch(epch), []eth2p0.ValidatorIndex{eth2p0.ValidatorIndex(vIdx)}) + require.NoError(t, err) + require.Len(t, duties, 1) + require.Equal(t, duties[0].PubKey, eth2Share) + }) + + t.Run("sync_committee_duties", func(t *testing.T) { + bmock.SyncCommitteeDutiesFunc = func(ctx context.Context, epoch eth2p0.Epoch, indices []eth2p0.ValidatorIndex) ([]*eth2v1.SyncCommitteeDuty, error) { + require.Equal(t, epoch, eth2p0.Epoch(epch)) + require.Equal(t, []eth2p0.ValidatorIndex{eth2p0.ValidatorIndex(vIdx)}, indices) + + return []*eth2v1.SyncCommitteeDuty{{ + PubKey: eth2Pubkey, + ValidatorIndex: vIdx, + }}, nil + } + + // Construct the validator api component + vapi, err := validatorapi.NewComponent(bmock, pubShareByKey, 0, "") + require.NoError(t, err) + duties, err := vapi.SyncCommitteeDuties(ctx, eth2p0.Epoch(epch), []eth2p0.ValidatorIndex{eth2p0.ValidatorIndex(vIdx)}) + require.NoError(t, err) + require.Len(t, duties, 1) + require.Equal(t, duties[0].PubKey, eth2Share) + }) } func TestComponent_SubmitValidatorRegistration(t *testing.T) {