Skip to content

Commit

Permalink
Feature: add rollup verification
Browse files Browse the repository at this point in the history
  • Loading branch information
aopoltorzhicky committed Jan 13, 2025
1 parent 2a5cb77 commit 1121a6a
Show file tree
Hide file tree
Showing 20 changed files with 393 additions and 20 deletions.
1 change: 1 addition & 0 deletions cmd/api/handler/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var (
errInvalidHashLength = errors.New("invalid hash: should be 32 bytes length")
errInvalidAddress = errors.New("invalid address")
errUnknownAddress = errors.New("unknown address")
errInvalidApiKey = errors.New("invalid api key")
errCancelRequest = "pq: canceling statement due to user request"
)

Expand Down
54 changes: 42 additions & 12 deletions cmd/api/handler/rollup_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package handler
import (
"context"
"encoding/base64"
"net/http"

"github.com/celenium-io/celestia-indexer/internal/storage"
"github.com/celenium-io/celestia-indexer/internal/storage/postgres"
Expand Down Expand Up @@ -43,7 +44,7 @@ type createRollupRequest struct {
Website string `json:"website" validate:"omitempty,url"`
GitHub string `json:"github" validate:"omitempty,url"`
Twitter string `json:"twitter" validate:"omitempty,url"`
Logo string `json:"logo" validate:"omitempty,url"`
Logo string `json:"logo" validate:"required,url"`
L2Beat string `json:"l2_beat" validate:"omitempty,url"`
DeFiLama string `json:"defi_lama" validate:"omitempty"`
Bridge string `json:"bridge" validate:"omitempty,eth_addr"`
Expand All @@ -65,22 +66,31 @@ type rollupProvider struct {
}

func (handler RollupAuthHandler) Create(c echo.Context) error {
val := c.Get(apiKeyName)
apiKey, ok := val.(storage.ApiKey)
if !ok {
return handleError(c, errInvalidApiKey, handler.address)
}

req, err := bindAndValidate[createRollupRequest](c)
if err != nil {
return badRequestError(c, err)
}

if err := handler.createRollup(c.Request().Context(), req); err != nil {
rollupId, err := handler.createRollup(c.Request().Context(), req, apiKey.Admin)
if err != nil {
return handleError(c, err, handler.rollups)
}

return success(c)
return c.JSON(http.StatusOK, echo.Map{
"id": rollupId,
})
}

func (handler RollupAuthHandler) createRollup(ctx context.Context, req *createRollupRequest) error {
func (handler RollupAuthHandler) createRollup(ctx context.Context, req *createRollupRequest, isAdmin bool) (uint64, error) {
tx, err := postgres.BeginTransaction(ctx, handler.tx)
if err != nil {
return err
return 0, err
}

rollup := storage.Rollup{
Expand All @@ -103,26 +113,30 @@ func (handler RollupAuthHandler) createRollup(ctx context.Context, req *createRo
Category: enums.RollupCategory(req.Category),
Slug: slug.Make(req.Name),
Tags: req.Tags,
Verified: isAdmin,
}

if err := tx.SaveRollup(ctx, &rollup); err != nil {
return tx.HandleError(ctx, err)
return 0, tx.HandleError(ctx, err)
}

providers, err := handler.createProviders(ctx, rollup.Id, req.Providers...)
if err != nil {
return tx.HandleError(ctx, err)
return 0, tx.HandleError(ctx, err)
}

if err := tx.SaveProviders(ctx, providers...); err != nil {
return tx.HandleError(ctx, err)
return 0, tx.HandleError(ctx, err)
}

if err := tx.RefreshLeaderboard(ctx); err != nil {
return tx.HandleError(ctx, err)
return 0, tx.HandleError(ctx, err)
}

return tx.Flush(ctx)
if err := tx.Flush(ctx); err != nil {
return 0, err
}
return rollup.Id, nil
}

func (handler RollupAuthHandler) createProviders(ctx context.Context, rollupId uint64, data ...rollupProvider) ([]storage.RollupProvider, error) {
Expand Down Expand Up @@ -178,19 +192,25 @@ type updateRollupRequest struct {
}

func (handler RollupAuthHandler) Update(c echo.Context) error {
val := c.Get(apiKeyName)
apiKey, ok := val.(storage.ApiKey)
if !ok {
return handleError(c, errInvalidApiKey, handler.address)
}

req, err := bindAndValidate[updateRollupRequest](c)
if err != nil {
return badRequestError(c, err)
}

if err := handler.updateRollup(c.Request().Context(), req); err != nil {
if err := handler.updateRollup(c.Request().Context(), req, apiKey.Admin); err != nil {
return handleError(c, err, handler.rollups)
}

return success(c)
}

func (handler RollupAuthHandler) updateRollup(ctx context.Context, req *updateRollupRequest) error {
func (handler RollupAuthHandler) updateRollup(ctx context.Context, req *updateRollupRequest, isAdmin bool) error {
tx, err := postgres.BeginTransaction(ctx, handler.tx)
if err != nil {
return err
Expand Down Expand Up @@ -221,6 +241,7 @@ func (handler RollupAuthHandler) updateRollup(ctx context.Context, req *updateRo
Category: enums.RollupCategory(req.Category),
Links: req.Links,
Tags: req.Tags,
Verified: isAdmin,
}

if err := tx.UpdateRollup(ctx, &rollup); err != nil {
Expand Down Expand Up @@ -254,6 +275,15 @@ type deleteRollupRequest struct {
}

func (handler RollupAuthHandler) Delete(c echo.Context) error {
val := c.Get(apiKeyName)
apiKey, ok := val.(storage.ApiKey)
if !ok {
return handleError(c, errInvalidApiKey, handler.address)
}
if !apiKey.Admin {
return handleError(c, errInvalidApiKey, handler.address)
}

req, err := bindAndValidate[deleteRollupRequest](c)
if err != nil {
return badRequestError(c, err)
Expand Down
25 changes: 25 additions & 0 deletions cmd/api/handler/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/base64"
"net/http"

"github.com/celenium-io/celestia-indexer/internal/storage"
"github.com/celenium-io/celestia-indexer/internal/storage/types"
pkgTypes "github.com/celenium-io/celestia-indexer/pkg/types"
"github.com/cosmos/cosmos-sdk/types/bech32"
Expand Down Expand Up @@ -119,3 +120,27 @@ func typeValidator() validator.Func {
return err == nil
}
}

type KeyValidator struct {
apiKeys storage.IApiKey
errChecker NoRows
}

func NewKeyValidator(apiKeys storage.IApiKey, errChecker NoRows) KeyValidator {
return KeyValidator{apiKeys: apiKeys, errChecker: errChecker}
}

const apiKeyName = "api_key"

func (kv KeyValidator) Validate(key string, c echo.Context) (bool, error) {
apiKey, err := kv.apiKeys.Get(c.Request().Context(), key)
if err != nil {
if kv.errChecker.IsNoRows(err) {
return false, nil
}
return false, err
}
c.Logger().Infof("using apikey: %s", apiKey.Description)
c.Set(apiKeyName, apiKey)
return true, nil
}
94 changes: 94 additions & 0 deletions cmd/api/handler/validators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@
package handler

import (
"database/sql"
"net/http"
"net/http/httptest"
"testing"

"github.com/celenium-io/celestia-indexer/internal/storage"
"github.com/celenium-io/celestia-indexer/internal/storage/mock"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)

func Test_isAddress(t *testing.T) {
Expand Down Expand Up @@ -72,3 +80,89 @@ func Test_isValoperAddress(t *testing.T) {
})
}
}

func TestKeyValidator_Validate(t *testing.T) {
t.Run("valid key", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
e := echo.New()
ctx := e.NewContext(req, rec)

ctrl := gomock.NewController(t)
defer ctrl.Finish()

errChecker := mock.NewMockIDelegation(ctrl)
apiKeys := mock.NewMockIApiKey(ctrl)
kv := NewKeyValidator(apiKeys, errChecker)

apiKeys.EXPECT().
Get(gomock.Any(), "valid").
Return(storage.ApiKey{
Key: "valid",
Description: "descr",
}, nil).
Times(1)

ok, err := kv.Validate("valid", ctx)
require.NoError(t, err)
require.True(t, ok)
})

t.Run("invalid key", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
e := echo.New()
ctx := e.NewContext(req, rec)

ctrl := gomock.NewController(t)
defer ctrl.Finish()

errChecker := mock.NewMockIDelegation(ctrl)
apiKeys := mock.NewMockIApiKey(ctrl)
kv := NewKeyValidator(apiKeys, errChecker)

apiKeys.EXPECT().
Get(gomock.Any(), "invalid").
Return(storage.ApiKey{}, sql.ErrNoRows).
Times(1)

errChecker.EXPECT().
IsNoRows(sql.ErrNoRows).
Return(true).
Times(1)

ok, err := kv.Validate("invalid", ctx)
require.NoError(t, err)
require.False(t, ok)
})

t.Run("unexpected error", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
e := echo.New()
ctx := e.NewContext(req, rec)

ctrl := gomock.NewController(t)
defer ctrl.Finish()

errChecker := mock.NewMockIDelegation(ctrl)
apiKeys := mock.NewMockIApiKey(ctrl)
kv := NewKeyValidator(apiKeys, errChecker)

unexpectedErr := errors.New("unexpected")

apiKeys.EXPECT().
Get(gomock.Any(), "invalid").
Return(storage.ApiKey{}, unexpectedErr).
Times(1)

errChecker.EXPECT().
IsNoRows(unexpectedErr).
Return(false).
Times(1)

ok, err := kv.Validate("invalid", ctx)
require.Error(t, err)
require.False(t, ok)
})
}
5 changes: 2 additions & 3 deletions cmd/api/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,11 +497,10 @@ func initHandlers(ctx context.Context, e *echo.Echo, cfg Config, db postgres.Sto

auth := v1.Group("/auth")
{
keyValidator := handler.NewKeyValidator(db.ApiKeys, db.BlobLogs)
keyMiddleware := middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{
KeyLookup: "header:Authorization",
Validator: func(key string, c echo.Context) (bool, error) {
return key == os.Getenv("API_AUTH_KEY"), nil
},
Validator: keyValidator.Validate,
})

rollupAuthHandler := handler.NewRollupAuthHandler(db.Rollup, db.Address, db.Namespace, db.Transactable)
Expand Down
2 changes: 2 additions & 0 deletions database/views/22_leaderboard.sql
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ CREATE MATERIALIZED VIEW IF NOT EXISTS leaderboard AS
group by 1, 2
) as agg
inner join rollup_provider as rp on rp.address_id = agg.signer_id AND (rp.namespace_id = agg.namespace_id OR rp.namespace_id = 0)
inner join rollup on rollup.id = rp.rollup_id
where rollup.verified = TRUE
group by 1
)
select
Expand Down
27 changes: 27 additions & 0 deletions internal/storage/api_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2024 PK Lab AG <[email protected]>
// SPDX-License-Identifier: MIT

package storage

import (
"context"

"github.com/uptrace/bun"
)

//go:generate mockgen -source=$GOFILE -destination=mock/$GOFILE -package=mock -typed
type IApiKey interface {
Get(ctx context.Context, key string) (ApiKey, error)
}

type ApiKey struct {
bun.BaseModel `bun:"apikey" comment:"Table with private api keys"`

Key string `bun:"key,pk,notnull" comment:"Key"`
Description string `bun:"description" comment:"Additional info about issuer and user"`
Admin bool `bun:"admin" comment:"Verified user"`
}

func (ApiKey) TableName() string {
return "apikey"
}
1 change: 1 addition & 0 deletions internal/storage/generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ var Models = []any{
&Rollup{},
&RollupProvider{},
&Grant{},
&ApiKey{},
}

//go:generate mockgen -source=$GOFILE -destination=mock/$GOFILE -package=mock -typed
Expand Down
Loading

0 comments on commit 1121a6a

Please sign in to comment.