Skip to content

Commit

Permalink
Feature: add rollup verification (#329)
Browse files Browse the repository at this point in the history
* Feature: add rollup verification

* Fix: change verification status

* Add admin middleware

* Add some useful enpoints
  • Loading branch information
aopoltorzhicky authored Jan 13, 2025
1 parent 2a5cb77 commit 53da19b
Show file tree
Hide file tree
Showing 23 changed files with 530 additions and 22 deletions.
34 changes: 34 additions & 0 deletions cmd/api/admin_middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// SPDX-FileCopyrightText: 2024 PK Lab AG <contact@pklab.io>
// SPDX-License-Identifier: MIT

package main

import (
"net/http"

"github.com/celenium-io/celestia-indexer/cmd/api/handler"
"github.com/celenium-io/celestia-indexer/internal/storage"
"github.com/labstack/echo/v4"
)

var accessDeniedErr = echo.Map{
"error": "access denied",
}

func AdminMiddleware() echo.MiddlewareFunc {
return checkOnAdminPermission
}

func checkOnAdminPermission(next echo.HandlerFunc) echo.HandlerFunc {
return func(ctx echo.Context) error {
val := ctx.Get(handler.ApiKeyName)
apiKey, ok := val.(storage.ApiKey)
if !ok {
return ctx.JSON(http.StatusForbidden, accessDeniedErr)
}
if !apiKey.Admin {
return ctx.JSON(http.StatusForbidden, accessDeniedErr)
}
return next(ctx)
}
}
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
94 changes: 82 additions & 12 deletions cmd/api/handler/rollup_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ package handler
import (
"context"
"encoding/base64"
"net/http"

"github.com/celenium-io/celestia-indexer/cmd/api/handler/responses"
"github.com/celenium-io/celestia-indexer/internal/storage"
"github.com/celenium-io/celestia-indexer/internal/storage/postgres"
enums "github.com/celenium-io/celestia-indexer/internal/storage/types"
Expand Down Expand Up @@ -43,7 +45,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 +67,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 +114,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 +193,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 +242,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 @@ -282,3 +304,51 @@ func (handler RollupAuthHandler) deleteRollup(ctx context.Context, id uint64) er

return tx.Flush(ctx)
}

func (handler RollupAuthHandler) Unverified(c echo.Context) error {
rollups, err := handler.rollups.Unverified(c.Request().Context())
if err != nil {
return handleError(c, err, handler.rollups)
}

response := make([]responses.Rollup, len(rollups))
for i := range rollups {
response[i] = responses.NewRollup(&rollups[i])
}

return returnArray(c, response)
}

type verifyRollupRequest struct {
Id uint64 `param:"id" validate:"required,min=1"`
}

func (handler RollupAuthHandler) Verify(c echo.Context) error {
req, err := bindAndValidate[verifyRollupRequest](c)
if err != nil {
return badRequestError(c, err)
}

if err := handler.verify(c.Request().Context(), req.Id); err != nil {
return handleError(c, err, handler.address)
}

return success(c)
}

func (handler RollupAuthHandler) verify(ctx context.Context, id uint64) error {
tx, err := postgres.BeginTransaction(ctx, handler.tx)
if err != nil {
return err
}

err = tx.UpdateRollup(ctx, &storage.Rollup{
Id: id,
Verified: true,
})
if err != nil {
return tx.HandleError(ctx, err)
}

return tx.Flush(ctx)
}
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)
})
}
10 changes: 6 additions & 4 deletions cmd/api/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,19 +497,21 @@ 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,
})
adminMiddleware := AdminMiddleware()

rollupAuthHandler := handler.NewRollupAuthHandler(db.Rollup, db.Address, db.Namespace, db.Transactable)
rollup := auth.Group("/rollup")
{
rollup.POST("/new", rollupAuthHandler.Create, keyMiddleware)
rollup.PATCH("/:id", rollupAuthHandler.Update, keyMiddleware)
rollup.DELETE("/:id", rollupAuthHandler.Delete, keyMiddleware)
rollup.DELETE("/:id", rollupAuthHandler.Delete, keyMiddleware, adminMiddleware)
rollup.PATCH("/:id/verify", rollupAuthHandler.Verify, keyMiddleware, adminMiddleware)
rollup.GET("/unverified", rollupAuthHandler.Unverified, keyMiddleware, adminMiddleware)
}
}

Expand Down
Loading

0 comments on commit 53da19b

Please sign in to comment.