diff --git a/backend/pkg/api/data_access/dummy.go b/backend/pkg/api/data_access/dummy.go index 7bee9a38c..b4c28420d 100644 --- a/backend/pkg/api/data_access/dummy.go +++ b/backend/pkg/api/data_access/dummy.go @@ -253,10 +253,6 @@ func (d *DummyService) GetValidatorDashboardGroupExists(ctx context.Context, das return true, nil } -func (d *DummyService) GetValidatorDashboardExistingValidatorCount(ctx context.Context, dashboardId t.VDBIdPrimary, validators []t.VDBValidator) (uint64, error) { - return getDummyData[uint64]() -} - func (d *DummyService) AddValidatorDashboardValidators(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, validators []t.VDBValidator) ([]t.VDBPostValidatorsData, error) { return getDummyData[[]t.VDBPostValidatorsData]() } diff --git a/backend/pkg/api/data_access/vdb.go b/backend/pkg/api/data_access/vdb.go index e7498702f..da907a294 100644 --- a/backend/pkg/api/data_access/vdb.go +++ b/backend/pkg/api/data_access/vdb.go @@ -28,7 +28,6 @@ type ValidatorDashboardRepository interface { GetValidatorDashboardGroupCount(ctx context.Context, dashboardId t.VDBIdPrimary) (uint64, error) GetValidatorDashboardGroupExists(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64) (bool, error) - GetValidatorDashboardExistingValidatorCount(ctx context.Context, dashboardId t.VDBIdPrimary, validators []t.VDBValidator) (uint64, error) AddValidatorDashboardValidators(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, validators []t.VDBValidator) ([]t.VDBPostValidatorsData, error) AddValidatorDashboardValidatorsByDepositAddress(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, address string, limit uint64) ([]t.VDBPostValidatorsData, error) AddValidatorDashboardValidatorsByWithdrawalAddress(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, address string, limit uint64) ([]t.VDBPostValidatorsData, error) diff --git a/backend/pkg/api/data_access/vdb_management.go b/backend/pkg/api/data_access/vdb_management.go index d415f0a23..b82bbff30 100644 --- a/backend/pkg/api/data_access/vdb_management.go +++ b/backend/pkg/api/data_access/vdb_management.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "fmt" "math/big" + "slices" "sort" "strconv" "strings" @@ -15,7 +16,6 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/gobitfly/beaconchain/pkg/api/enums" t "github.com/gobitfly/beaconchain/pkg/api/types" - "github.com/gobitfly/beaconchain/pkg/commons/log" "github.com/gobitfly/beaconchain/pkg/commons/utils" constypes "github.com/gobitfly/beaconchain/pkg/consapi/types" "github.com/lib/pq" @@ -790,21 +790,6 @@ func (d *DataAccessService) GetValidatorDashboardGroupExists(ctx context.Context return groupExists, err } -// return how many of the passed validators are already in the dashboard -func (d *DataAccessService) GetValidatorDashboardExistingValidatorCount(ctx context.Context, dashboardId t.VDBIdPrimary, validators []t.VDBValidator) (uint64, error) { - if len(validators) == 0 { - return 0, nil - } - - var count uint64 - err := d.alloyReader.GetContext(ctx, &count, ` - SELECT COUNT(*) - FROM users_val_dashboards_validators - WHERE dashboard_id = $1 AND validator_index = ANY($2) - `, dashboardId, pq.Array(validators)) - return count, err -} - func (d *DataAccessService) AddValidatorDashboardValidators(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, validators []t.VDBValidator) ([]t.VDBPostValidatorsData, error) { if len(validators) == 0 { // No validators to add @@ -889,191 +874,145 @@ func (d *DataAccessService) AddValidatorDashboardValidators(ctx context.Context, return result, nil } +// Updates the group for validators already in the dashboard linked to the deposit address. +// Adds up to limit new validators associated with the deposit address, if not already in the dashboard. func (d *DataAccessService) AddValidatorDashboardValidatorsByDepositAddress(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, address string, limit uint64) ([]t.VDBPostValidatorsData, error) { - // for all validators already in the dashboard that are associated with the deposit address, update the group - // then add no more than `limit` validators associated with the deposit address to the dashboard addressParsed, err := hex.DecodeString(strings.TrimPrefix(address, "0x")) if err != nil { return nil, err } - if len(addressParsed) != 20 { - return nil, fmt.Errorf("invalid deposit address: %s", address) - } - var validatorIndicesToAdd []uint64 - err = d.readerDb.SelectContext(ctx, &validatorIndicesToAdd, "SELECT validatorindex FROM validators WHERE pubkey IN (SELECT publickey FROM eth1_deposits WHERE from_address = $1) ORDER BY validatorindex LIMIT $2;", addressParsed, limit) - if err != nil { - return nil, err - } + g, gCtx := errgroup.WithContext(ctx) - // retrieve the existing validators - var existingValidators []uint64 - err = d.alloyWriter.SelectContext(ctx, &existingValidators, "SELECT validator_index FROM users_val_dashboards_validators WHERE dashboard_id = $1", dashboardId) - if err != nil { - return nil, err - } - existingValidatorsMap := make(map[uint64]bool, len(existingValidators)) - for _, validatorIndex := range existingValidators { - existingValidatorsMap[validatorIndex] = true - } - - // filter out the validators that are already in the dashboard + // fetch validators that are already in the dashboard and associated with the deposit address var validatorIndicesToUpdate []uint64 + + g.Go(func() error { + return d.readerDb.SelectContext(gCtx, &validatorIndicesToUpdate, ` + SELECT DISTINCT uvdv.validator_index + FROM validators v + JOIN eth1_deposits d ON v.pubkey = d.publickey + JOIN users_val_dashboards_validators uvdv ON v.validatorindex = uvdv.validator_index + WHERE uvdv.dashboard_id = $1 AND d.from_address = $2; + `, dashboardId, addressParsed) + }) + + // fetch validators that are not yet in the dashboard and associated with the deposit address, up to the limit var validatorIndicesToInsert []uint64 - for _, validatorIndex := range validatorIndicesToAdd { - if _, ok := existingValidatorsMap[validatorIndex]; ok { - validatorIndicesToUpdate = append(validatorIndicesToUpdate, validatorIndex) - } else { - validatorIndicesToInsert = append(validatorIndicesToInsert, validatorIndex) - } - } + g.Go(func() error { + return d.readerDb.SelectContext(gCtx, &validatorIndicesToInsert, ` + SELECT DISTINCT v.validatorindex + FROM validators v + JOIN eth1_deposits d ON v.pubkey = d.publickey + LEFT JOIN users_val_dashboards_validators uvdv ON v.validatorindex = uvdv.validator_index AND uvdv.dashboard_id = $1 + WHERE d.from_address = $2 AND uvdv.validator_index IS NULL + ORDER BY v.validatorindex + LIMIT $3; + `, dashboardId, addressParsed, limit) + }) - // update the group for all existing validators - validatorIndices := make([]uint64, 0, int(limit)) - validatorIndices = append(validatorIndices, validatorIndicesToUpdate...) - - // insert the new validators up to the allowed user max limit taking into account how many validators are already in the dashboard - if len(validatorIndicesToInsert) > 0 { - freeSpace := int(limit) - len(existingValidators) - if freeSpace > 0 { - if len(validatorIndicesToInsert) > freeSpace { // cap inserts to the amount of free space available - log.Infof("limiting the number of validators to insert to %d", freeSpace) - validatorIndicesToInsert = validatorIndicesToInsert[:freeSpace] - } - validatorIndices = append(validatorIndices, validatorIndicesToInsert...) - } + err = g.Wait() + if err != nil { + return nil, err } - if len(validatorIndices) == 0 { - // no validators to add - return []t.VDBPostValidatorsData{}, nil - } - log.Infof("inserting %d new validators and updating %d validators of dashboard %d, limit is %d", len(validatorIndicesToInsert), len(validatorIndicesToUpdate), dashboardId, limit) + validatorIndices := slices.Concat(validatorIndicesToUpdate, validatorIndicesToInsert) + return d.AddValidatorDashboardValidators(ctx, dashboardId, groupId, validatorIndices) } +// Updates the group for validators already in the dashboard linked to the withdrawal address. +// Adds up to limit new validators associated with the withdrawal address, if not already in the dashboard. func (d *DataAccessService) AddValidatorDashboardValidatorsByWithdrawalAddress(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, address string, limit uint64) ([]t.VDBPostValidatorsData, error) { - // for all validators already in the dashboard that are associated with the withdrawal address, update the group - // then add no more than `limit` validators associated with the deposit address to the dashboard addressParsed, err := hex.DecodeString(strings.TrimPrefix(address, "0x")) if err != nil { return nil, err } - var validatorIndicesToAdd []uint64 - err = d.readerDb.SelectContext(ctx, &validatorIndicesToAdd, "SELECT validatorindex FROM validators WHERE withdrawalcredentials = $1 ORDER BY validatorindex LIMIT $2;", addressParsed, limit) - if err != nil { - return nil, err - } - // retrieve the existing validators - var existingValidators []uint64 - err = d.alloyWriter.SelectContext(ctx, &existingValidators, "SELECT validator_index FROM users_val_dashboards_validators WHERE dashboard_id = $1", dashboardId) - if err != nil { - return nil, err - } - existingValidatorsMap := make(map[uint64]bool, len(existingValidators)) - for _, validatorIndex := range existingValidators { - existingValidatorsMap[validatorIndex] = true - } + g, gCtx := errgroup.WithContext(ctx) - // filter out the validators that are already in the dashboard + // fetch validators that are already in the dashboard and associated with the withdrawal address var validatorIndicesToUpdate []uint64 + g.Go(func() error { + return d.readerDb.SelectContext(gCtx, &validatorIndicesToUpdate, ` + SELECT DISTINCT uvdv.validator_index + FROM validators v + JOIN users_val_dashboards_validators uvdv ON v.validatorindex = uvdv.validator_index + WHERE uvdv.dashboard_id = $1 AND v.withdrawalcredentials = $2 AND uvdv.dashboard_id = $2; + `, dashboardId, addressParsed) + }) + + // fetch validators that are not yet in the dashboard and associated with the withdrawal address, up to the limit var validatorIndicesToInsert []uint64 - for _, validatorIndex := range validatorIndicesToAdd { - if _, ok := existingValidatorsMap[validatorIndex]; ok { - validatorIndicesToUpdate = append(validatorIndicesToUpdate, validatorIndex) - } else { - validatorIndicesToInsert = append(validatorIndicesToInsert, validatorIndex) - } - } + g.Go(func() error { + return d.readerDb.SelectContext(gCtx, &validatorIndicesToInsert, ` + SELECT DISTINCT v.validatorindex + FROM validators v + LEFT JOIN users_val_dashboards_validators uvdv ON v.validatorindex = uvdv.validator_index AND uvdv.dashboard_id = $1 + WHERE v.withdrawalcredentials = $2 AND uvdv.validator_index IS NULL + ORDER BY v.validatorindex + LIMIT $3; + `, dashboardId, addressParsed, limit) + }) - // update the group for all existing validators - validatorIndices := make([]uint64, 0, int(limit)) - validatorIndices = append(validatorIndices, validatorIndicesToUpdate...) - - // insert the new validators up to the allowed user max limit taking into account how many validators are already in the dashboard - if len(validatorIndicesToInsert) > 0 { - freeSpace := int(limit) - len(existingValidators) - if freeSpace > 0 { - if len(validatorIndicesToInsert) > freeSpace { // cap inserts to the amount of free space available - log.Infof("limiting the number of validators to insert to %d", freeSpace) - validatorIndicesToInsert = validatorIndicesToInsert[:freeSpace] - } - validatorIndices = append(validatorIndices, validatorIndicesToInsert...) - } + err = g.Wait() + if err != nil { + return nil, err } - if len(validatorIndices) == 0 { - // no validators to add - return []t.VDBPostValidatorsData{}, nil - } - log.Infof("inserting %d new validators and updating %d validators of dashboard %d, limit is %d", len(validatorIndicesToInsert), len(validatorIndicesToUpdate), dashboardId, limit) + validatorIndices := slices.Concat(validatorIndicesToUpdate, validatorIndicesToInsert) + return d.AddValidatorDashboardValidators(ctx, dashboardId, groupId, validatorIndices) } +// Update the group for validators already in the dashboard linked to the graffiti (via produced block). +// Add up to limit new validators associated with the graffiti, if not already in the dashboard. func (d *DataAccessService) AddValidatorDashboardValidatorsByGraffiti(ctx context.Context, dashboardId t.VDBIdPrimary, groupId uint64, graffiti string, limit uint64) ([]t.VDBPostValidatorsData, error) { - // for all validators already in the dashboard that are associated with the graffiti (by produced block), update the group - // then add no more than `limit` validators associated with the deposit address to the dashboard - var validatorIndicesToAdd []uint64 - err := d.readerDb.SelectContext(ctx, &validatorIndicesToAdd, "SELECT DISTINCT proposer FROM blocks WHERE graffiti_text = $1 ORDER BY proposer LIMIT $2;", graffiti, limit) - if err != nil { - return nil, err - } + g, gCtx := errgroup.WithContext(ctx) - // retrieve the existing validators - var existingValidators []uint64 - err = d.alloyWriter.SelectContext(ctx, &existingValidators, "SELECT validator_index FROM users_val_dashboards_validators WHERE dashboard_id = $1", dashboardId) - if err != nil { - return nil, err - } - existingValidatorsMap := make(map[uint64]bool, len(existingValidators)) - for _, validatorIndex := range existingValidators { - existingValidatorsMap[validatorIndex] = true - } - - // filter out the validators that are already in the dashboard + // fetch validators that are already in the dashboard and associated with the graffiti var validatorIndicesToUpdate []uint64 + g.Go(func() error { + return d.readerDb.SelectContext(gCtx, &validatorIndicesToUpdate, ` + SELECT DISTINCT uvdv.validator_index + FROM blocks b + JOIN users_val_dashboards_validators uvdv ON b.proposer = uvdv.validator_index + WHERE uvdv.dashboard_id = $1 AND b.graffiti_text = $2; + `, dashboardId, graffiti) + }) + + // fetch validators that are not yet in the dashboard and associated with the graffiti, up to the limit var validatorIndicesToInsert []uint64 - for _, validatorIndex := range validatorIndicesToAdd { - if _, ok := existingValidatorsMap[validatorIndex]; ok { - validatorIndicesToUpdate = append(validatorIndicesToUpdate, validatorIndex) - } else { - validatorIndicesToInsert = append(validatorIndicesToInsert, validatorIndex) - } - } + g.Go(func() error { + return d.readerDb.SelectContext(gCtx, &validatorIndicesToInsert, ` + SELECT DISTINCT b.proposer + FROM blocks b + LEFT JOIN users_val_dashboards_validators uvdv ON b.proposer = uvdv.validator_index AND uvdv.dashboard_id = $1 + WHERE b.graffiti_text = $2 AND uvdv.validator_index IS NULL + ORDER BY b.proposer + LIMIT $3; + `, dashboardId, graffiti, limit) + }) - // update the group for all existing validators - validatorIndices := make([]uint64, 0, int(limit)) - validatorIndices = append(validatorIndices, validatorIndicesToUpdate...) - - // insert the new validators up to the allowed user max limit taking into account how many validators are already in the dashboard - if len(validatorIndicesToInsert) > 0 { - freeSpace := int(limit) - len(existingValidators) - if freeSpace > 0 { - if len(validatorIndicesToInsert) > freeSpace { // cap inserts to the amount of free space available - log.Infof("limiting the number of validators to insert to %d", freeSpace) - validatorIndicesToInsert = validatorIndicesToInsert[:freeSpace] - } - validatorIndices = append(validatorIndices, validatorIndicesToInsert...) - } + err := g.Wait() + if err != nil { + return nil, err } - if len(validatorIndices) == 0 { - // no validators to add - return []t.VDBPostValidatorsData{}, nil - } - log.Infof("inserting %d new validators and updating %d validators of dashboard %d, limit is %d", len(validatorIndicesToInsert), len(validatorIndicesToUpdate), dashboardId, limit) + validatorIndices := slices.Concat(validatorIndicesToUpdate, validatorIndicesToInsert) + return d.AddValidatorDashboardValidators(ctx, dashboardId, groupId, validatorIndices) } func (d *DataAccessService) RemoveValidatorDashboardValidators(ctx context.Context, dashboardId t.VDBIdPrimary, validators []t.VDBValidator) error { if len(validators) == 0 { - // // Remove all validators for the dashboard - // _, err := d.alloyWriter.ExecContext(ctx, ` - // DELETE FROM users_val_dashboards_validators - // WHERE dashboard_id = $1 - // `, dashboardId) - return fmt.Errorf("calling RemoveValidatorDashboardValidators with empty validators list is not allowed") + // Remove all validators for the dashboard + // This is usually forbidden by API validation + _, err := d.alloyWriter.ExecContext(ctx, ` + DELETE FROM users_val_dashboards_validators + WHERE dashboard_id = $1 + `, dashboardId) + return err } //Create the query to delete validators diff --git a/backend/pkg/api/handlers/public.go b/backend/pkg/api/handlers/public.go index e0e560b8c..6bfd928b2 100644 --- a/backend/pkg/api/handlers/public.go +++ b/backend/pkg/api/handlers/public.go @@ -6,7 +6,6 @@ import ( "fmt" "math" "net/http" - "reflect" "time" "github.com/gobitfly/beaconchain/pkg/api/enums" @@ -491,16 +490,15 @@ func (h *HandlerService) PublicDeleteValidatorDashboardGroup(w http.ResponseWrit // PublicGetValidatorDashboardGroups godoc // -// @Description Add new validators to a specified dashboard or update the group of already-added validators. +// @Description Add new validators to a specified dashboard or update the group of already-added validators. This endpoint will always add as many validators as possible, even if more validators are provided than allowed by the subscription plan. The response will contain a list of added validators. // @Security ApiKeyInHeader || ApiKeyInQuery // @Tags Validator Dashboard Management // @Accept json // @Produce json // @Param dashboard_id path string true "The ID of the dashboard." -// @Param request body handlers.PublicPostValidatorDashboardValidators.request true "`group_id`: (optional) Provide a single group id, to which all validators get added to. If omitted, the default group will be used.

To add validators, only one of the following fields can be set:" +// @Param request body handlers.PublicPostValidatorDashboardValidators.request true "`group_id`: (optional) Provide a single group id, to which all validators get added to. If omitted, the default group will be used.

To add validators or update their group, only one of the following fields can be set:" // @Success 201 {object} types.ApiDataResponse[[]types.VDBPostValidatorsData] "Returns a list of added validators." // @Failure 400 {object} types.ApiErrorResponse -// @Failure 409 {object} types.ApiErrorResponse "Conflict. The request could not be performed by the server because the authenticated user has already reached their validator limit." // @Router /validator-dashboards/{dashboard_id}/validators [post] func (h *HandlerService) PublicPostValidatorDashboardValidators(w http.ResponseWriter, r *http.Request) { var v validationError @@ -512,7 +510,9 @@ func (h *HandlerService) PublicPostValidatorDashboardValidators(w http.ResponseW WithdrawalAddress string `json:"withdrawal_address,omitempty"` Graffiti string `json:"graffiti,omitempty"` } - var req request + req := request{ + GroupId: types.DefaultGroupId, // default value + } if err := v.checkBody(&req, r); err != nil { handleErr(w, r, err) return @@ -521,11 +521,17 @@ func (h *HandlerService) PublicPostValidatorDashboardValidators(w http.ResponseW handleErr(w, r, v) return } + groupId := req.GroupId // check if exactly one of validators, deposit_address, withdrawal_address, graffiti is set - fields := []interface{}{req.Validators, req.DepositAddress, req.WithdrawalAddress, req.Graffiti} + nilFields := []bool{ + req.Validators == nil, + req.DepositAddress == "", + req.WithdrawalAddress == "", + req.Graffiti == "", + } var count int - for _, set := range fields { - if !reflect.ValueOf(set).IsZero() { + for _, isNil := range nilFields { + if !isNil { count++ } } @@ -537,7 +543,6 @@ func (h *HandlerService) PublicPostValidatorDashboardValidators(w http.ResponseW return } - groupId := req.GroupId ctx := r.Context() groupExists, err := h.dai.GetValidatorDashboardGroupExists(ctx, dashboardId, groupId) if err != nil { @@ -558,11 +563,23 @@ func (h *HandlerService) PublicPostValidatorDashboardValidators(w http.ResponseW handleErr(w, r, err) return } - limit := userInfo.PremiumPerks.ValidatorsPerDashboard if req.Validators == nil && !userInfo.PremiumPerks.BulkAdding && !isUserAdmin(userInfo) { - returnConflict(w, r, errors.New("bulk adding not allowed with current subscription plan")) + returnForbidden(w, r, errors.New("bulk adding not allowed with current subscription plan")) + return + } + dashboardLimit := userInfo.PremiumPerks.ValidatorsPerDashboard + existingValidatorCount, err := h.dai.GetValidatorDashboardValidatorsCount(ctx, dashboardId) + if err != nil { + handleErr(w, r, err) return } + var limit uint64 + if isUserAdmin(userInfo) { + limit = math.MaxUint32 // no limit for admins + } else if dashboardLimit >= existingValidatorCount { + limit = dashboardLimit - existingValidatorCount + } + var data []types.VDBPostValidatorsData var dataErr error switch { @@ -577,15 +594,8 @@ func (h *HandlerService) PublicPostValidatorDashboardValidators(w http.ResponseW handleErr(w, r, err) return } - // check if adding more validators than allowed - existingValidatorCount, err := h.dai.GetValidatorDashboardExistingValidatorCount(ctx, dashboardId, validators) - if err != nil { - handleErr(w, r, err) - return - } - if uint64(len(validators)) > existingValidatorCount+limit { - returnConflict(w, r, fmt.Errorf("adding more validators than allowed, limit is %v new validators", limit)) - return + if len(validators) > int(limit) { + validators = validators[:limit] } data, dataErr = h.dai.AddValidatorDashboardValidators(ctx, dashboardId, groupId, validators) @@ -688,7 +698,7 @@ func (h *HandlerService) PublicDeleteValidatorDashboardValidators(w http.Respons handleErr(w, r, err) return } - indices, publicKeys := v.checkValidators(req.Validators, false) + indices, publicKeys := v.checkValidators(req.Validators, forbidEmpty) if v.hasErrors() { handleErr(w, r, v) return