Skip to content

Commit

Permalink
exporter: decideds endpoint; fix signers; fix resolving role; cache d…
Browse files Browse the repository at this point in the history
…omain; fix convert (#1766)

* draft

* add comment

* use runner role instead beacon role

* lint fix

* add log

* run exporter post-fork

* support hex strings with '0x' prefix for public keys in exporter API

* cleanups

* revert to beacon role

* use trim

* adapt to older response format

* fix JSON capital case

* change type of From and To

* fix error text if values are equal

* continue if one of pubkeys/duties has no participants

* allow saving participants not equal to 3f+1

* save participants if quorum is longer than existing one

* remove redundant check

* add uint in Bind

* consider failure to bind params as a bad request

* fix role order in convert package

* Revert "fix role order in convert package"

This reverts commit 50c86a5.

* Revert "fix JSON capital case"

This reverts commit d8da4a7.

* use casts

* go fmt

* Revert "allow saving participants not equal to 3f+1"

This reverts commit 1a53ee6.

* Revert "save participants if quorum is longer than existing one"

This reverts commit bd94915.

* allow saving participants not equal to 3f+1

* add logs

* Revert "add logs"

This reverts commit e373882.

* add tmp log

* adjust log

* use duty store instead of root to determine existence of duty

* Revert "use duty store instead of root to determine existence of duty"

This reverts commit 59b3f7a.

* save both attester and sync committee roots

* sort imports

* handle case when both attester and sync committee roles exist

* add msg_id log

* fix validator PK logging in CommitteeObserver

* fix role order in convert package

* Revert "fix role order in convert package"

This reverts commit c31192d.

* fix casting issues

* process all message types by CommitteeObserver

* disable ErrTooManyDutiesPerEpoch

* attempt to fix issues with conversion to convert runner role

* Revert "process all message types by CommitteeObserver"

This reverts commit 04f7db2.

* add log when no roles

* fix saving sync committee roots

* logs on saving block root

* log root on saving

* use attestation data hash

* use signed attestation data and block root for checking beacon role

* check err after signing

* add validator log when saving participants

* more logs

* improve logs

* save roots for all committee indices

* more logs

* more logs for committee index

* fix committee index logs

* more committee index logs

* set max committee index to 128

* Revert "set max committee index to 128"

This reverts commit aa0baa0.

* fix data race

* fix typo on mutex unlock

* identifier logs

* global root store

* revert disabling ErrTooManyDutiesPerEpoch

* cleanup logs

* fix exporter unit tests

* use duty store instead of storing roots

* add comments

* leftovers

* revert non role committee logic

* Revert "revert non role committee logic"

This reverts commit 7de9236.

* revert remobing validator index log fiekd

* adjust log

* get rid of Root in processMessage

* fix remaining casting issues between roles

* Revert "use duty store instead of storing roots"

This reverts commit 8378f8f

* Revert "get rid of Root in processMessage"

This reverts commit b3f63ea

* cache DomainData calls

* fix TestNewController

* add logs when saving roots

* Revert "add logs when saving roots"

This reverts commit e091617.

* fix event syncer tests

* fix TestHandleBlockEventsStream

* avoid redundant root computations

* Delete root_cache.go

* improve handling non-existing roles

* wrap errors into api.Error

* merge quorums

* revert config change

* approve spec diff

---------

Co-authored-by: Nikita Kryuchkov <nkryuchkov10@gmail.com>
Co-authored-by: moshe-blox <moshe@blox.io>
  • Loading branch information
3 people authored Oct 30, 2024
1 parent 73e99f5 commit a020f5a
Show file tree
Hide file tree
Showing 25 changed files with 663 additions and 112 deletions.
6 changes: 6 additions & 0 deletions api/bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ func Bind(r *http.Request, dest interface{}) error {
return err
}
fieldValue.SetInt(v)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
v, err := strconv.ParseUint(formValue, 10, 64)
if err != nil {
return err
}
fieldValue.SetUint(v)
case reflect.Float32, reflect.Float64:
v, err := strconv.ParseFloat(formValue, 64)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion api/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func (e *ErrorResponse) Error() string {
return e.Err.Error()
}

func InvalidRequestError(err error) *ErrorResponse {
func BadRequestError(err error) *ErrorResponse {
return &ErrorResponse{
Err: err,
Code: 400,
Expand Down
119 changes: 119 additions & 0 deletions api/handlers/exporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package handlers

import (
"fmt"
"net/http"

"github.com/attestantio/go-eth2-client/spec/phase0"
spectypes "github.com/ssvlabs/ssv-spec/types"

"github.com/ssvlabs/ssv/api"
exporterapi "github.com/ssvlabs/ssv/exporter/api"
"github.com/ssvlabs/ssv/exporter/convert"
ibftstorage "github.com/ssvlabs/ssv/ibft/storage"
qbftstorage "github.com/ssvlabs/ssv/protocol/v2/qbft/storage"
"github.com/ssvlabs/ssv/utils/casts"
)

type Exporter struct {
DomainType spectypes.DomainType
QBFTStores *ibftstorage.QBFTStores
}

type ParticipantResponse struct {
Role string `json:"role"`
Slot uint64 `json:"slot"`
PublicKey string `json:"public_key"`
Message struct {
// We're keeping "Signers" capitalized to avoid breaking existing clients that rely on the current structure
Signers []uint64 `json:"Signers"`
} `json:"message"`
}

func (e *Exporter) Decideds(w http.ResponseWriter, r *http.Request) error {
var request struct {
From uint64 `json:"from"`
To uint64 `json:"to"`
Roles api.RoleSlice `json:"roles"`
PubKeys api.HexSlice `json:"pubkeys"`
}
var response struct {
Data []*ParticipantResponse `json:"data"`
}

if err := api.Bind(r, &request); err != nil {
return api.BadRequestError(err)
}

if request.From > request.To {
return api.BadRequestError(fmt.Errorf("'from' must be less than or equal to 'to'"))
}

if len(request.PubKeys) == 0 {
return api.BadRequestError(fmt.Errorf("at least one public key is required"))
}

if len(request.Roles) == 0 {
return api.BadRequestError(fmt.Errorf("at least one role is required"))
}

response.Data = []*ParticipantResponse{}

qbftStores := make(map[convert.RunnerRole]qbftstorage.QBFTStore, len(request.Roles))
for _, role := range request.Roles {
runnerRole := casts.BeaconRoleToConvertRole(spectypes.BeaconRole(role))
storage := e.QBFTStores.Get(runnerRole)
if storage == nil {
return api.Error(fmt.Errorf("role storage doesn't exist: %v", role))
}

qbftStores[runnerRole] = storage
}

for _, role := range request.Roles {
runnerRole := casts.BeaconRoleToConvertRole(spectypes.BeaconRole(role))
qbftStore := qbftStores[runnerRole]

for _, pubKey := range request.PubKeys {
msgID := convert.NewMsgID(e.DomainType, pubKey, runnerRole)
from := phase0.Slot(request.From)
to := phase0.Slot(request.To)

participantsList, err := qbftStore.GetParticipantsInRange(msgID, from, to)
if err != nil {
return api.Error(fmt.Errorf("error getting participants: %w", err))
}

if len(participantsList) == 0 {
continue
}

data, err := exporterapi.ParticipantsAPIData(participantsList...)
if err != nil {
return api.Error(fmt.Errorf("error getting participants API data: %w", err))
}

apiData, ok := data.([]*exporterapi.ParticipantsAPI)
if !ok {
return api.Error(fmt.Errorf("invalid type for participants API data"))
}

for _, apiMsg := range apiData {
response.Data = append(response.Data, transformToParticipantResponse(apiMsg))
}
}
}

return api.Render(w, r, response)
}

func transformToParticipantResponse(apiMsg *exporterapi.ParticipantsAPI) *ParticipantResponse {
response := &ParticipantResponse{
Role: apiMsg.Role,
Slot: uint64(apiMsg.Slot),
PublicKey: apiMsg.ValidatorPK,
}
response.Message.Signers = apiMsg.Signers

return response
}
24 changes: 20 additions & 4 deletions api/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/ssvlabs/ssv/api"
"github.com/ssvlabs/ssv/api/handlers"
"github.com/ssvlabs/ssv/utils/commons"
)

type Server struct {
Expand All @@ -19,19 +20,22 @@ type Server struct {

node *handlers.Node
validators *handlers.Validators
exporter *handlers.Exporter
}

func New(
logger *zap.Logger,
addr string,
node *handlers.Node,
validators *handlers.Validators,
exporter *handlers.Exporter,
) *Server {
return &Server{
logger: logger,
addr: addr,
node: node,
validators: validators,
exporter: exporter,
}
}

Expand All @@ -41,20 +45,25 @@ func (s *Server) Run() error {
router.Use(middleware.Throttle(runtime.NumCPU() * 4))
router.Use(middleware.Compress(5, "application/json"))
router.Use(middlewareLogger(s.logger))
router.Use(middlewareNodeVersion)

router.Get("/v1/node/identity", api.Handler(s.node.Identity))
router.Get("/v1/node/peers", api.Handler(s.node.Peers))
router.Get("/v1/node/topics", api.Handler(s.node.Topics))
router.Get("/v1/node/health", api.Handler(s.node.Health))
router.Get("/v1/validators", api.Handler(s.validators.List))
// We kept both GET and POST methods to ensure compatibility and avoid breaking changes for clients that may rely on either method
router.Get("/v1/exporter/decideds", api.Handler(s.exporter.Decideds))
router.Post("/v1/exporter/decideds", api.Handler(s.exporter.Decideds))

s.logger.Info("Serving SSV API", zap.String("addr", s.addr))

server := &http.Server{
Addr: s.addr,
Handler: router,
ReadTimeout: 12 * time.Second,
WriteTimeout: 12 * time.Second,
Addr: s.addr,
Handler: router,
ReadHeaderTimeout: 10 * time.Second,
ReadTimeout: 12 * time.Second,
WriteTimeout: 12 * time.Second,
}
return server.ListenAndServe()
}
Expand All @@ -80,3 +89,10 @@ func middlewareLogger(logger *zap.Logger) func(next http.Handler) http.Handler {
return http.HandlerFunc(fn)
}
}

func middlewareNodeVersion(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-SSV-Node-Version", commons.GetNodeVersion())
next.ServeHTTP(w, r)
})
}
55 changes: 49 additions & 6 deletions api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ package api

import (
"encoding/hex"
"encoding/json"
"errors"
"strconv"
"strings"

spectypes "github.com/ssvlabs/ssv-spec/types"

"github.com/ssvlabs/ssv/protocol/v2/message"
)

type Hex []byte
Expand All @@ -17,18 +22,15 @@ func (h *Hex) UnmarshalJSON(data []byte) error {
if len(data) < 2 || data[0] != '"' || data[len(data)-1] != '"' {
return errors.New("invalid hex string")
}
b, err := hex.DecodeString(string(data[1 : len(data)-1]))
if err != nil {
return err
}
*h = b
return nil
str := string(data[1 : len(data)-1])
return h.Bind(str)
}

func (h *Hex) Bind(value string) error {
if value == "" {
return nil
}
value = strings.TrimPrefix(value, "0x")
b, err := hex.DecodeString(value)
if err != nil {
return err
Expand Down Expand Up @@ -69,3 +71,44 @@ func (us *Uint64Slice) Bind(value string) error {
}
return nil
}

type Role spectypes.BeaconRole

func (r *Role) Bind(value string) error {
role, err := message.BeaconRoleFromString(value)
if err != nil {
return err
}
*r = Role(role)
return nil
}

func (r Role) MarshalJSON() ([]byte, error) {
return []byte(`"` + spectypes.BeaconRole(r).String() + `"`), nil
}

func (r *Role) UnmarshalJSON(data []byte) error {
var role string
err := json.Unmarshal(data, &role)
if err != nil {
return err
}
return r.Bind(role)
}

type RoleSlice []Role

func (rs *RoleSlice) Bind(value string) error {
if value == "" {
return nil
}
for _, s := range strings.Split(value, ",") {
var r Role
err := r.Bind(s)
if err != nil {
return err
}
*rs = append(*rs, r)
}
return nil
}
4 changes: 4 additions & 0 deletions cli/operator/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,10 @@ var StartNodeCmd = &cobra.Command{
&handlers.Validators{
Shares: nodeStorage.Shares(),
},
&handlers.Exporter{
DomainType: networkConfig.AlanDomainType,
QBFTStores: storageMap,
},
)
go func() {
err := apiServer.Run()
Expand Down
1 change: 1 addition & 0 deletions eth/eventhandler/event_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1407,6 +1407,7 @@ func setupEventHandler(t *testing.T, ctx context.Context, logger *zap.Logger, ne
bc := beacon.NewMockBeaconNode(ctrl)
validatorCtrl := validator.NewController(logger, validator.ControllerOptions{
Context: ctx,
NetworkConfig: *network,
DB: db,
RegistryStorage: nodeStorage,
BeaconSigner: keyManager,
Expand Down
1 change: 1 addition & 0 deletions eth/eventsyncer/event_syncer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ func setupEventHandler(
bc := beacon.NewMockBeaconNode(ctrl)
validatorCtrl := validator.NewController(logger, validator.ControllerOptions{
Context: ctx,
NetworkConfig: testNetworkConfig,
DB: db,
RegistryStorage: nodeStorage,
OperatorDataStore: operatorDataStore,
Expand Down
2 changes: 1 addition & 1 deletion exporter/api/query_handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func TestHandleDecidedQuery(t *testing.T) {
convert.RoleCommittee,
convert.RoleProposer,
convert.RoleAggregator,
convert.RoleSyncCommitteeContribution,
convert.RoleSyncCommittee,
// skipping spectypes.BNRoleSyncCommitteeContribution to test non-existing storage
}
_, ibftStorage := newStorageForTest(db, l, roles...)
Expand Down
1 change: 0 additions & 1 deletion exporter/convert/roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const (
RoleProposer
RoleSyncCommitteeContribution
RoleSyncCommittee

RoleValidatorRegistration
RoleVoluntaryExit
RoleCommittee
Expand Down
6 changes: 2 additions & 4 deletions ibft/storage/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,16 @@ package storage
import (
"encoding/binary"
"fmt"

"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
specqbft "github.com/ssvlabs/ssv-spec/qbft"
spectypes "github.com/ssvlabs/ssv-spec/types"
"github.com/ssvlabs/ssv/exporter/convert"
"go.uber.org/zap"

"github.com/ssvlabs/ssv/exporter/convert"
"github.com/ssvlabs/ssv/protocol/v2/qbft/instance"
qbftstorage "github.com/ssvlabs/ssv/protocol/v2/qbft/storage"
"github.com/ssvlabs/ssv/storage/basedb"
Expand Down Expand Up @@ -242,9 +243,6 @@ func uInt64ToByteSlice(n uint64) []byte {
}

func encodeOperators(operators []spectypes.OperatorID) ([]byte, error) {
if len(operators) != 4 && len(operators) != 7 && len(operators) != 13 {
return nil, fmt.Errorf("invalid operators list size: %d", len(operators))
}
encoded := make([]byte, len(operators)*8)
for i, v := range operators {
binary.BigEndian.PutUint64(encoded[i*8:], v)
Expand Down
2 changes: 1 addition & 1 deletion message/validation/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestMessageValidator_maxRound(t *testing.T) {
},
{
name: "Unknown role",
role: spectypes.RunnerRole(999),
role: 999,
want: 0,
err: fmt.Errorf("unknown role"),
},
Expand Down
Loading

0 comments on commit a020f5a

Please sign in to comment.