Skip to content

Commit

Permalink
feat/test: endpoint for overwriting routes
Browse files Browse the repository at this point in the history
  • Loading branch information
p0mvn committed Jan 9, 2024
1 parent 6a92dfa commit ba94351
Show file tree
Hide file tree
Showing 10 changed files with 519 additions and 371 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"-test.timeout",
"30m",
"-test.run",
"TestRouterTestSuite/TestGetOptimalQuote_Cache_Overwrites",
"TestRouterTestSuite/TestOverwriteRoutes",
"-test.v"
],
},
Expand Down
8 changes: 7 additions & 1 deletion app/sidecar_query_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ type sideCarQueryServer struct {
logger log.Logger
}

const (
// This is a directory path where the overwrite routes are backed up in case of failure.
// On restart, the overwrite routes are restored from this directory.
overwriteRoutesPath = "overwrite_routes"
)

// GetTokensUseCase implements SideCarQueryServer.
func (sqs *sideCarQueryServer) GetTokensUseCase() domain.TokensUsecase {
return sqs.tokensUseCase
Expand Down Expand Up @@ -147,7 +153,7 @@ func NewSideCarQueryServer(appCodec codec.Codec, routerConfig domain.RouterConfi

// Initialize router repository, usecase and HTTP handler
routerRepository := routerredisrepo.New(redisTxManager, routerConfig.RouteCacheExpirySeconds)
routerUsecase := routerUseCase.NewRouterUsecase(timeoutContext, routerRepository, poolsUseCase, routerConfig, logger, cache.New(), routesOverwrite)
routerUsecase := routerUseCase.WithOverwriteRoutesPath(routerUseCase.NewRouterUsecase(timeoutContext, routerRepository, poolsUseCase, routerConfig, logger, cache.New(), routesOverwrite), overwriteRoutesPath)
routerHttpDelivery.NewRouterHandler(e, routerUsecase, logger)

// Initialize system handler
Expand Down
8 changes: 8 additions & 0 deletions domain/mvc/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,12 @@ type RouterUsecase interface {
GetCachedCandidateRoutes(ctx context.Context, tokenInDenom, tokenOutDenom string) (sqsdomain.CandidateRoutes, error)
// StoreRoutes stores all router state in the files locally. Used for debugging.
StoreRouterStateFiles(ctx context.Context) error
// OverwriteRoutes overwrites the routes for the given tokenIn and tokenOutDenom with the given candidateRoutes.
// Returns error if:
// - The routes are invalid
// * No pool exists
// * Denom mismatch in route
// * Denom does not exist in pool
// * Token out mismatch across routes
OverwriteRoutes(ctx context.Context, tokeinInDenom string, candidateRoutes []sqsdomain.CandidateRoute) error
}
51 changes: 47 additions & 4 deletions router/delivery/http/router_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package http

import (
"errors"
"io"
"net/http"
"strconv"
"strings"
Expand All @@ -14,6 +15,8 @@ import (
"github.com/osmosis-labs/sqs/domain"
"github.com/osmosis-labs/sqs/domain/mvc"
"github.com/osmosis-labs/sqs/log"
"github.com/osmosis-labs/sqs/sqsdomain"
"github.com/osmosis-labs/sqs/sqsdomain/json"
)

// ResponseError represent the response error struct
Expand Down Expand Up @@ -46,6 +49,7 @@ func NewRouterHandler(e *echo.Echo, us mvc.RouterUsecase, logger log.Logger) {
e.GET(formatRouterResource("/custom-quote"), handler.GetCustomQuote)
e.GET(formatRouterResource("/taker-fee-pool/:id"), handler.GetTakerFee)
e.POST(formatRouterResource("/store-state"), handler.StoreRouterStateInFiles)
e.POST(formatRouterResource("/overwrite-route"), handler.OverwriteRoute)
}

// GetOptimalQuote will determine the optimal quote for a given tokenIn and tokenOutDenom
Expand Down Expand Up @@ -189,6 +193,35 @@ func (a *RouterHandler) StoreRouterStateInFiles(c echo.Context) error {
return c.JSON(http.StatusOK, "Router state stored in files")
}

// TODO: authentication for the endpoint and enable only in dev mode.
func (a *RouterHandler) OverwriteRoute(c echo.Context) error {
ctx := c.Request().Context()

// Get the tokenInDenom denom string
tokenInDenom, err := getValidTokenInStr(c)
if err != nil {
return c.JSON(getStatusCode(err), ResponseError{Message: err.Error()})
}

// Read the request body
body, err := io.ReadAll(c.Request().Body)
if err != nil {
return c.String(http.StatusInternalServerError, "Error reading request body")
}

// Parse the request body
var routes []sqsdomain.CandidateRoute
if err := json.Unmarshal(body, &routes); err != nil {
return c.String(http.StatusInternalServerError, "Error parsing request body")
}

if err := a.RUsecase.OverwriteRoutes(ctx, tokenInDenom, routes); err != nil {
return c.JSON(getStatusCode(err), ResponseError{Message: err.Error()})
}

return c.JSON(http.StatusOK, "Router state stored in files")
}

func getStatusCode(err error) int {
if err == nil {
return http.StatusOK
Expand Down Expand Up @@ -222,14 +255,24 @@ func getValidRoutingParameters(c echo.Context) (string, sdk.Coin, error) {
return tokenOutStr, tokenIn, nil
}

func getValidTokenInTokenOutStr(c echo.Context) (tokenOutStr, tokenInStr string, err error) {
tokenInStr = c.QueryParam("tokenIn")
tokenOutStr = c.QueryParam("tokenOutDenom")
func getValidTokenInStr(c echo.Context) (string, error) {
tokenInStr := c.QueryParam("tokenIn")

if len(tokenInStr) == 0 {
return "", "", errors.New("tokenIn is required")
return "", errors.New("tokenIn is required")
}

return tokenInStr, nil
}

func getValidTokenInTokenOutStr(c echo.Context) (tokenOutStr, tokenInStr string, err error) {
tokenInStr, err = getValidTokenInStr(c)
if err != nil {
return "", "", err
}

tokenOutStr = c.QueryParam("tokenOutDenom")

if len(tokenOutStr) == 0 {
return "", "", errors.New("tokenOutDenom is required")
}
Expand Down
6 changes: 3 additions & 3 deletions router/usecase/optimized_routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,7 @@ func (s *RouterTestSuite) TestGetOptimalQuote_Mainnet() {
router, tickMap, takerFeeMap := s.setupMainnetRouter(config)

// Mock router use case.
routerUsecase, _ := s.setupRouterAndPoolsUsecase(router, tc.tokenInDenom, tc.tokenOutDenom, tickMap, takerFeeMap, cache.New(), cache.NewNoOpRoutesOverwrite())
routerUsecase, _ := s.setupRouterAndPoolsUsecase(router, tickMap, takerFeeMap, cache.New(), cache.NewNoOpRoutesOverwrite())

// System under test
quote, err := routerUsecase.GetOptimalQuote(context.Background(), sdk.NewCoin(tc.tokenInDenom, tc.amountIn), tc.tokenOutDenom)
Expand Down Expand Up @@ -738,7 +738,7 @@ func (s *RouterTestSuite) TestGetCustomQuote_Mainnet_UOSMOUION() {
// - converting candidate routes to routes with all the necessary data.
// COTRACT: router is initialized with setupMainnetRouter(...) or setupDefaultMainnetRouter(...)
func (s *RouterTestSuite) constructRoutesFromMainnetPools(router *routerusecase.Router, tokenInDenom, tokenOutDenom string, tickMap map[uint64]sqsdomain.TickModel, takerFeeMap sqsdomain.TakerFeeMap) []route.RouteImpl {
_, poolsUsecase := s.setupRouterAndPoolsUsecase(router, tokenInDenom, tokenOutDenom, tickMap, takerFeeMap, cache.New(), cache.NewNoOpRoutesOverwrite())
_, poolsUsecase := s.setupRouterAndPoolsUsecase(router, tickMap, takerFeeMap, cache.New(), cache.NewNoOpRoutesOverwrite())

candidateRoutes, err := router.GetCandidateRoutes(tokenInDenom, tokenOutDenom)
s.Require().NoError(err)
Expand All @@ -751,7 +751,7 @@ func (s *RouterTestSuite) constructRoutesFromMainnetPools(router *routerusecase.

// Sets up and returns usecases for router and pools by mocking the mainnet data
// from json files.
func (s *RouterTestSuite) setupRouterAndPoolsUsecase(router *routerusecase.Router, tokenInDenom, tokenOutDenom string, tickMap map[uint64]sqsdomain.TickModel, takerFeeMap sqsdomain.TakerFeeMap, rankedRoutesCache *cache.Cache, routesOverwrite *cache.RoutesOverwrite) (mvc.RouterUsecase, mvc.PoolsUsecase) {
func (s *RouterTestSuite) setupRouterAndPoolsUsecase(router *routerusecase.Router, tickMap map[uint64]sqsdomain.TickModel, takerFeeMap sqsdomain.TakerFeeMap, rankedRoutesCache *cache.Cache, routesOverwrite *cache.RoutesOverwrite) (mvc.RouterUsecase, mvc.PoolsUsecase) {
// Setup router repository mock
routerRepositoryMock := sqsdomainmocks.RedisRouterRepositoryMock{}
routerusecase.WithRouterRepository(router, &routerRepositoryMock)
Expand Down
87 changes: 87 additions & 0 deletions router/usecase/router_usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ package usecase

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/prometheus/client_golang/prometheus"
"go.uber.org/zap"

"github.com/osmosis-labs/osmosis/osmomath"
"github.com/osmosis-labs/osmosis/osmoutils"
poolmanagertypes "github.com/osmosis-labs/osmosis/v21/x/poolmanager/types"
"github.com/osmosis-labs/sqs/domain"
"github.com/osmosis-labs/sqs/domain/cache"
Expand All @@ -20,6 +23,7 @@ import (
"github.com/osmosis-labs/sqs/router/usecase/routertesting/parsing"
"github.com/osmosis-labs/sqs/sqsdomain"
routerredisrepo "github.com/osmosis-labs/sqs/sqsdomain/repository/redis/router"
"github.com/osmosis-labs/sqs/sqsutil"
)

var _ mvc.RouterUsecase = &routerUseCaseImpl{}
Expand All @@ -34,6 +38,11 @@ type routerUseCaseImpl struct {
routesOverwrite *cache.RoutesOverwrite

rankedRouteCache *cache.Cache

// This is a path where the overwrite routes are stored as backup in case of failure.
// On restart, the routes are loaded from this path.
// It is defined on the use case for testability (s.t. we can set a temp path in tests)
overwriteRoutesPath string
}

const (
Expand Down Expand Up @@ -77,6 +86,12 @@ func NewRouterUsecase(timeout time.Duration, routerRepository routerredisrepo.Ro
}
}

// WithOverwriteRoutesPath sets the overwrite routes path on the router use case.
func WithOverwriteRoutesPath(routerUsecase mvc.RouterUsecase, overwriteRoutesPath string) mvc.RouterUsecase {
routerUsecase.(*routerUseCaseImpl).overwriteRoutesPath = overwriteRoutesPath
return routerUsecase
}

// GetOptimalQuote returns the optimal quote by estimating the optimal route(s) through pools
// on the osmosis network.
// Uses caching strategies for optimal performance.
Expand Down Expand Up @@ -573,6 +588,78 @@ func (r *routerUseCaseImpl) StoreRouterStateFiles(ctx context.Context) error {
return nil
}

// OverwriteRoutes implements mvc.RouterUsecase.
func (r *routerUseCaseImpl) OverwriteRoutes(ctx context.Context, tokeinInDenom string, routes []sqsdomain.CandidateRoute) error {
if len(routes) == 0 {
return errors.New("routes cannot be empty")
}

// Find the unique pool IDs
uniquePoolIDs := make(map[uint64]struct{})

var (
// The token out denom that we expect to be the same for all routes
// We initialize it to token out denom of the first route and then validate
// that it equals for all other routes.
expectedTokenOutDenom string
// The token out denom of the previous pool
// For the first pool in route, assumed to be tokenInDenom
previousPoolsTokenOutDenom = tokeinInDenom
)
for i, route := range routes {
for _, pool := range route.Pools {
// Validate that token in is present in the first pool
poolData, err := r.poolsUsecase.GetPool(ctx, pool.ID)
if err != nil {
return err
}

poolDenoms := poolData.GetPoolDenoms()
if !osmoutils.Contains(poolDenoms, previousPoolsTokenOutDenom) {
return fmt.Errorf("token in denom %s not found in pool %d of route with index %d", tokeinInDenom, pool.ID, i)
}

// Persist unique pool ID
uniquePoolIDs[pool.ID] = struct{}{}

previousPoolsTokenOutDenom = pool.TokenOutDenom
}

// Make sure that the token out denom of the previous route is the same as for current route
// That is, all routes have the same token out denom
if i != 0 {
if expectedTokenOutDenom != previousPoolsTokenOutDenom {
return fmt.Errorf("token out denom %s does not match expected token out denom %s for route with index %d", previousPoolsTokenOutDenom, expectedTokenOutDenom, i)
}
}
expectedTokenOutDenom = previousPoolsTokenOutDenom
}

// Create the overwrite data structure that we save in cache
candidateRoutes := sqsdomain.CandidateRoutes{
Routes: routes,
UniquePoolIDs: uniquePoolIDs,
}

// Note that we only persist overwrite in one direction (tokenInDenom -> tokenOutDenom)
// For other directions, we must resubmit the request with denoms inversed.
overwriteKey := formatRouteCacheKey(tokeinInDenom, expectedTokenOutDenom)
r.routesOverwrite.Set(overwriteKey, candidateRoutes)

// Also save this thing to a file for crash recovery
bz, err := json.Marshal(candidateRoutes)
if err != nil {
return err
}

err = sqsutil.WriteBytes(r.overwriteRoutesPath, url.PathEscape(overwriteKey), bz)
if err != nil {
return err
}

return nil
}

// formatRouteCacheKey formats the given token in and token out denoms to a string.
func formatRouteCacheKey(tokenInDenom string, tokenOutDenom string) string {
return fmt.Sprintf("%s/%s", tokenInDenom, tokenOutDenom)
Expand Down
Loading

0 comments on commit ba94351

Please sign in to comment.