Skip to content

Commit

Permalink
fix: market map validation (#228)
Browse files Browse the repository at this point in the history
* add note:

* field len

* field len

* fix

* tickerstr

* route

* big

---------

Co-authored-by: David Terpay <[email protected]>
  • Loading branch information
Alex Johnson and davidterpay authored Mar 21, 2024
1 parent 4acd691 commit e2a1969
Show file tree
Hide file tree
Showing 14 changed files with 140 additions and 12 deletions.
16 changes: 15 additions & 1 deletion pkg/types/currency_pair.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import (
)

const (
ethereum = "ETHEREUM"
ethereum = "ETHEREUM"
MaxCPFieldLength = 128
)

// NewCurrencyPair returns a new CurrencyPair with the given base and quote strings.
Expand All @@ -31,9 +32,22 @@ func (cp *CurrencyPair) ValidateBasic() error {
if strings.ToUpper(cp.Quote) != cp.Quote {
return fmt.Errorf("incorrectly formatted quote string, expected: %s got: %s", strings.ToUpper(cp.Quote), cp.Quote)
}

if len(cp.Base) > MaxCPFieldLength || len(cp.Quote) > MaxCPFieldLength {
return fmt.Errorf("string field exceeds maximum length of %d", MaxCPFieldLength)
}

return nil
}

// Invert returns an inverted version of cp (where the Base and Quote are swapped).
func (cp *CurrencyPair) Invert() CurrencyPair {
return CurrencyPair{
Base: cp.Quote,
Quote: cp.Base,
}
}

// String returns a string representation of the CurrencyPair, in the following form "ETH/BTC".
func (cp CurrencyPair) String() string {
return fmt.Sprintf("%s/%s", cp.Base, cp.Quote)
Expand Down
17 changes: 17 additions & 0 deletions pkg/types/currency_pair_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/require"

slinkytypes "github.com/skip-mev/slinky/pkg/types"
"github.com/skip-mev/slinky/testutil"
)

func TestValidateBasic(t *testing.T) {
Expand Down Expand Up @@ -46,6 +47,22 @@ func TestValidateBasic(t *testing.T) {
},
false,
},
{
"Base is too long - fail",
slinkytypes.CurrencyPair{
Base: testutil.RandomString(slinkytypes.MaxCPFieldLength + 1),
Quote: "AA",
},
false,
},
{
"Quote is too long - fail",
slinkytypes.CurrencyPair{
Base: "BB",
Quote: testutil.RandomString(slinkytypes.MaxCPFieldLength + 1),
},
false,
},
{
"if both Quote + Base are formatted correctly - pass",
slinkytypes.CurrencyPair{
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -194,15 +194,15 @@ require (
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/mod v0.15.0 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/oauth2 v0.16.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.18.0 // indirect
golang.org/x/tools v0.19.0 // indirect
google.golang.org/api v0.162.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,7 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down Expand Up @@ -1449,6 +1450,7 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
13 changes: 13 additions & 0 deletions testutil/testutil.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package testutil

import "math/rand"

// RandomString generates a random string of length N.
func RandomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
result := make([]byte, length)
for i := range result {
result[i] = charset[rand.Intn(len(charset))]
}
return string(result)
}
8 changes: 5 additions & 3 deletions x/marketmap/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,18 @@ func (k *Keeper) GetAllTickersMap(ctx sdk.Context) (map[string]types.Ticker, err
// CreateTicker initializes a new Ticker.
// The Ticker.String corresponds to a market, and must be unique.
func (k *Keeper) CreateTicker(ctx sdk.Context, ticker types.Ticker) error {
tickerString := types.TickerString(ticker.String())

// Check if Ticker already exists for the provider
alreadyExists, err := k.tickers.Has(ctx, types.TickerString(ticker.String()))
alreadyExists, err := k.tickers.Has(ctx, tickerString)
if err != nil {
return err
}
if alreadyExists {
return types.NewTickerAlreadyExistsError(types.TickerString(ticker.String()))
return types.NewTickerAlreadyExistsError(tickerString)
}
// Create the config
return k.tickers.Set(ctx, types.TickerString(ticker.String()), ticker)
return k.tickers.Set(ctx, tickerString, ticker)
}

// GetAllProvidersMap returns the set of Providers objects currently stored in state
Expand Down
4 changes: 2 additions & 2 deletions x/marketmap/types/market.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func (mm *MarketMap) String() string {
// ValidateIndexPriceAggregation validates the market map configuration and its expected configuration for
// this aggregator. In particular, this will
//
// 1. Ensure that the market map is valid (ValidateBasic). This ensure's that each of the provider's
// 1. Ensure that the market map is valid (ValidateBasic). This ensures that each of the provider's
// markets are supported by the market map.
// 2. Ensure that each path has a corresponding ticker.
// 3. Ensure that each path has a valid number of operations.
Expand All @@ -84,7 +84,7 @@ func ValidateIndexPriceAggregation(
marketMap MarketMap,
) error {
for ticker, paths := range marketMap.Paths {
// The ticker must be supported by the market map. Otherwise we do not how to resolve the
// The ticker must be supported by the market map. Otherwise, we do not how to resolve the
// prices.
if _, ok := marketMap.Tickers[ticker]; !ok {
return fmt.Errorf("path includes a ticker that is not supported: %s", ticker)
Expand Down
2 changes: 1 addition & 1 deletion x/marketmap/types/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func (m *MsgUpdateMarketMap) ValidateBasic() error {
)
}

seenProviders := make(map[string]struct{})
seenProviders := make(map[string]struct{}, len(market.Providers.Providers))
for _, provider := range market.Providers.Providers {
if err := provider.ValidateBasic(); err != nil {
return err
Expand Down
8 changes: 7 additions & 1 deletion x/marketmap/types/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ func (p *Path) ValidateBasic() error {
return err
}

if op.Invert {
if _, ok := seen[op.CurrencyPair.Invert()]; ok {
return fmt.Errorf("duplicated pair found")
}
}

if _, ok := seen[op.CurrencyPair]; ok {
return fmt.Errorf("path is not a directed acyclic graph")
}
Expand Down Expand Up @@ -166,7 +172,7 @@ func (p *Paths) ValidateBasic(cp slinkytypes.CurrencyPair) error {

route := path.ShowRoute()
if _, ok := routes[route]; ok {
return fmt.Errorf("duplicate path found: %s", route)
return fmt.Errorf("duplicate route found: %s", route)
}
routes[route] = struct{}{}

Expand Down
25 changes: 25 additions & 0 deletions x/marketmap/types/path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ var (
},
}

usdteth = types.Ticker{
CurrencyPair: slinkytypes.CurrencyPair{
Base: "USDT",
Quote: "ETHEREUM",
},
Decimals: 8,
MinProviderCount: 1,
}

tickers = map[string]types.Ticker{
btcusdt.String(): btcusdt,
usdcusd.String(): usdcusd,
Expand Down Expand Up @@ -319,6 +328,22 @@ func TestPath(t *testing.T) {
target: "",
expErr: true,
},
{
name: "invalid path with multiple operations inverted duplicate",
path: types.Path{
Operations: []types.Operation{
{
CurrencyPair: ethusdt.CurrencyPair,
},
{
CurrencyPair: usdteth.CurrencyPair,
Invert: true,
},
},
},
target: "",
expErr: true,
},
{
name: "valid path with a single operation",
path: types.Path{
Expand Down
13 changes: 13 additions & 0 deletions x/marketmap/types/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import (
"fmt"
)

const (
MaxProviderNameFieldLength = 128
MaxProviderTickerFieldLength = 256
)

// ValidateBasic performs basic validation on Providers.
func (p *Providers) ValidateBasic() error {
for _, provider := range p.Providers {
Expand All @@ -21,10 +26,18 @@ func (pc *ProviderConfig) ValidateBasic() error {
return fmt.Errorf("provider name must not be empty")
}

if len(pc.Name) > MaxProviderNameFieldLength {
return fmt.Errorf("provider length is longer than maximum length of %d", MaxProviderNameFieldLength)
}

if len(pc.OffChainTicker) == 0 {
return fmt.Errorf("provider offchain ticker must not be empty")
}

if len(pc.OffChainTicker) > MaxProviderTickerFieldLength {
return fmt.Errorf("provider offchain ticker is longer than maximum length of %d", MaxProviderTickerFieldLength)
}

return nil
}

Expand Down
19 changes: 17 additions & 2 deletions x/marketmap/types/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/stretchr/testify/require"

"github.com/skip-mev/slinky/testutil"
"github.com/skip-mev/slinky/x/marketmap/types"
)

Expand All @@ -16,20 +17,34 @@ func TestProviderConfigValidateBasic(t *testing.T) {
}
require.NoError(t, pc.ValidateBasic())
})
t.Run("invalid name - fail", func(t *testing.T) {
t.Run("invalid empty name - fail", func(t *testing.T) {
pc := types.ProviderConfig{
Name: "",
OffChainTicker: "ticker",
}
require.Error(t, pc.ValidateBasic())
})
t.Run("valid offchain ticker - fail", func(t *testing.T) {
t.Run("invalid empty offchain ticker - fail", func(t *testing.T) {
pc := types.ProviderConfig{
Name: "mexc",
OffChainTicker: "",
}
require.Error(t, pc.ValidateBasic())
})
t.Run("invalid too long name - fail", func(t *testing.T) {
pc := types.ProviderConfig{
Name: testutil.RandomString(types.MaxProviderNameFieldLength + 1),
OffChainTicker: "ticker",
}
require.Error(t, pc.ValidateBasic())
})
t.Run("invalid too long offchain ticker - fail", func(t *testing.T) {
pc := types.ProviderConfig{
Name: "mexc",
OffChainTicker: testutil.RandomString(types.MaxProviderTickerFieldLength + 1),
}
require.Error(t, pc.ValidateBasic())
})
}

func TestProvidersEqual(t *testing.T) {
Expand Down
6 changes: 6 additions & 0 deletions x/marketmap/types/ticker.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const (
DefaultMinProviderCount = 1
// MaxPathLength is the maximum length of a path for a ticker conversion.
MaxPathLength = 3
// MaxMetadataJSONFieldLength is the maximum length of the MetadataJSON field.
MaxMetadataJSONFieldLength = 16384
)

// NewTicker returns a new Ticker instance. A Ticker represents a price feed for
Expand Down Expand Up @@ -51,6 +53,10 @@ func (t *Ticker) ValidateBasic() error {
return err
}

if len(t.Metadata_JSON) > MaxMetadataJSONFieldLength {
return fmt.Errorf("metadata json field is longer than maximum length of %d", MaxMetadataJSONFieldLength)
}

return json.IsValid([]byte(t.Metadata_JSON))
}

Expand Down
15 changes: 15 additions & 0 deletions x/marketmap/types/ticker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package types_test
import (
"testing"

"github.com/skip-mev/slinky/testutil"

"github.com/stretchr/testify/require"

slinkytypes "github.com/skip-mev/slinky/pkg/types"
Expand All @@ -27,6 +29,19 @@ func TestTicker(t *testing.T) {
},
expErr: false,
},
{
name: "invalid metadata length",
ticker: types.Ticker{
CurrencyPair: slinkytypes.CurrencyPair{
Base: "BITCOIN",
Quote: "USDT",
},
Decimals: 8,
MinProviderCount: 1,
Metadata_JSON: testutil.RandomString(types.MaxMetadataJSONFieldLength + 1),
},
expErr: true,
},
{
name: "empty base",
ticker: types.Ticker{
Expand Down

0 comments on commit e2a1969

Please sign in to comment.