Skip to content

Commit 8eadb2e

Browse files
BE-714 | Optimize route finding algorithm (#637)
BE-714 | Optimize route finding algorithm
1 parent b09162e commit 8eadb2e

16 files changed

+281
-163
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ Ref: https://keepachangelog.com/en/1.0.0/
3535

3636
# Changelog
3737

38+
## v28.3.1
39+
40+
- #637 - BE-714 | Optimize route finding algorithm
41+
3842
## v28.3.0
3943

4044
- Remove dependency on keyring, wire in CosmosSigner and grab private key from SQS_PRIVATE_KEY environment variable.

domain/candidate_routes.go

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package domain
22

33
import (
4-
sdk "github.com/cosmos/cosmos-sdk/types"
4+
"github.com/osmosis-labs/sqs/domain/osmomath"
55
ingesttypes "github.com/osmosis-labs/sqs/ingest/types"
6+
7+
osmoingesttypes "github.com/osmosis-labs/osmosis/v28/ingest/types"
8+
9+
sdk "github.com/cosmos/cosmos-sdk/types"
610
)
711

812
// CandidateRoutePoolFiltrerCb defines a candidate route pool filter
913
// that takes in a pool and returns true if the pool should be skipped.
10-
type CandidateRoutePoolFiltrerCb func(*ingesttypes.PoolWrapper) bool
14+
type CandidateRoutePoolFiltrerCb func(CandidatePoolWrapper) bool
1115

1216
// CandidateRouteSearchOptions represents the options for finding candidate routes.
1317
type CandidateRouteSearchOptions struct {
@@ -30,7 +34,7 @@ type CandidateRouteSearchOptions struct {
3034

3135
// ShouldSkipPool returns true if the candidate route algorithm should skip
3236
// a given pool by matching at least one of the pool filters
33-
func (c CandidateRouteSearchOptions) ShouldSkipPool(pool *ingesttypes.PoolWrapper) bool {
37+
func (c CandidateRouteSearchOptions) ShouldSkipPool(pool CandidatePoolWrapper) bool {
3438
for _, filter := range c.PoolFiltersAnyOf {
3539
if filter(pool) {
3640
return true
@@ -47,19 +51,18 @@ type CandidateRoutePoolIDFilterOptionCb struct {
4751
}
4852

4953
// ShouldSkipPool returns true of the given pool has ID that is present in c.PoolIDsToSkip
50-
func (c CandidateRoutePoolIDFilterOptionCb) ShouldSkipPool(pool *ingesttypes.PoolWrapper) bool {
51-
poolID := pool.GetId()
54+
func (c CandidateRoutePoolIDFilterOptionCb) ShouldSkipPool(pool CandidatePoolWrapper) bool {
55+
poolID := pool.ID
5256
_, ok := c.PoolIDsToSkip[poolID]
5357
return ok
5458
}
5559

60+
// ShouldSkipOrderbookPool skips orderbook pools
61+
// by returning true if pool.SQSModel.CosmWasmPoolModel is not nil
62+
// and pool.SQSModel.CosmWasmPoolModel.IsOrderbook() returns true.
5663
var (
57-
// ShouldSkipOrderbookPool skips orderbook pools
58-
// by returning true if pool.SQSModel.CosmWasmPoolModel is not nil
59-
// and pool.SQSModel.CosmWasmPoolModel.IsOrderbook() returns true.
60-
ShouldSkipOrderbookPool CandidateRoutePoolFiltrerCb = func(pool *ingesttypes.PoolWrapper) bool {
61-
cosmWasmPoolModel := pool.SQSModel.CosmWasmPoolModel
62-
return cosmWasmPoolModel != nil && cosmWasmPoolModel.IsOrderbook()
64+
ShouldSkipOrderbookPool CandidateRoutePoolFiltrerCb = func(pool CandidatePoolWrapper) bool {
65+
return pool.IsOrderbook
6366
}
6467
)
6568

@@ -76,12 +79,33 @@ type CandidateRouteSearcher interface {
7679
FindCandidateRoutesInGivenOut(tokenOut sdk.Coin, tokenInDenom string, options CandidateRouteSearchOptions) (ingesttypes.CandidateRoutes, error)
7780
}
7881

79-
// CandidateRouteDenomData represents the data for a candidate route for a given denom.
82+
// CandidateRouteDenomData represents data structure that contains pool data
83+
// required for the candidate route algorithm.
84+
type CandidatePoolWrapper struct {
85+
ID uint64
86+
PoolDenoms []string
87+
PoolLiquidityCap uint64 // Note: the value is truncated if it is larger than uint64
88+
Balances sdk.Coins
89+
IsAlloyTransmuter bool
90+
IsOrderbook bool
91+
}
92+
93+
func NewCandidatePoolWrapper(id uint64, p osmoingesttypes.SQSPool) CandidatePoolWrapper {
94+
return CandidatePoolWrapper{
95+
ID: id,
96+
PoolDenoms: p.PoolDenoms,
97+
PoolLiquidityCap: osmomath.SafeUint64(p.PoolLiquidityCap),
98+
Balances: p.Balances,
99+
IsAlloyTransmuter: p.CosmWasmPoolModel != nil && p.CosmWasmPoolModel.IsAlloyTransmuter(),
100+
IsOrderbook: p.CosmWasmPoolModel != nil && p.CosmWasmPoolModel.IsOrderbook(),
101+
}
102+
}
103+
80104
type CandidateRouteDenomData struct {
81105
// SortedPools is the sorted list of pools for the denom.
82-
SortedPools []ingesttypes.PoolI
106+
SortedPools []CandidatePoolWrapper
83107
// CanonicalOrderbooks is the map of canonical orderbooks keyed by the pair token.
84108
// For example if this is candidate route denom data for OSMO and there is a canonical orderbook with ID 23
85109
// for ATOM/OSMO, we would have an entry from ATOM to 23 in this map.
86-
CanonicalOrderbooks map[string]ingesttypes.PoolI
110+
CanonicalOrderbooks map[string]CandidatePoolWrapper
87111
}

domain/candidate_routes_test.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
// It then validates that if at least one of the filters is matched, ShouldSkipPool()
1919
// would return true
2020
func TestCandidateRouteSearchOptions_ShouldSkipPool(t *testing.T) {
21-
2221
const (
2322
defaultPoolID = uint64(1)
2423
)
@@ -39,7 +38,6 @@ func TestCandidateRouteSearchOptions_ShouldSkipPool(t *testing.T) {
3938
Version: cosmwasmpool.ORDERBOOK_MIN_CONTRACT_VERSION,
4039
},
4140
Data: cosmwasmpool.CosmWasmPoolData{
42-
4341
Orderbook: &cosmwasmpool.OrderbookData{},
4442
},
4543
},
@@ -110,12 +108,16 @@ func TestCandidateRouteSearchOptions_ShouldSkipPool(t *testing.T) {
110108
for _, tc := range tests {
111109
tc := tc
112110
t.Run(tc.name, func(t *testing.T) {
113-
114111
// Set up pool ID filter
115112
poolIDFilter := domain.CandidateRoutePoolIDFilterOptionCb{
116113
PoolIDsToSkip: tc.poolIDsToFilter,
117114
}
118115

116+
pool := domain.NewCandidatePoolWrapper(
117+
tc.poolToTest.GetId(),
118+
tc.poolToTest.GetSQSPoolModel(),
119+
)
120+
119121
// Initialize options with 2 filters:
120122
// 1. By pool ID
121123
// 2. All order books.
@@ -127,7 +129,7 @@ func TestCandidateRouteSearchOptions_ShouldSkipPool(t *testing.T) {
127129
}
128130

129131
// System under test.
130-
shouldSkip := opts.ShouldSkipPool(&tc.poolToTest)
132+
shouldSkip := opts.ShouldSkipPool(pool)
131133

132134
// Validate result.
133135
require.Equal(t, tc.expectedShouldSkip, shouldSkip)

domain/mocks/candidate_route_data_holder_mock.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,27 @@ import (
66
)
77

88
type CandidateRouteSearchDataHolderMock struct {
9-
CandidateRouteSearchData map[string]domain.CandidateRouteDenomData
9+
CandidateRouteSearchData map[string]*domain.CandidateRouteDenomData
10+
Error error
1011
}
1112

1213
var _ mvc.CandidateRouteSearchDataHolder = &CandidateRouteSearchDataHolderMock{}
1314

1415
// GetCandidateRouteSearchData implements mvc.CandidateRouteSearchDataHolder.
15-
func (c *CandidateRouteSearchDataHolderMock) GetCandidateRouteSearchData() map[string]domain.CandidateRouteDenomData {
16-
return c.CandidateRouteSearchData
16+
func (c *CandidateRouteSearchDataHolderMock) GetCandidateRouteSearchData() (map[string]*domain.CandidateRouteDenomData, error) {
17+
return c.CandidateRouteSearchData, c.Error
1718
}
1819

1920
// SetCandidateRouteSearchData implements mvc.CandidateRouteSearchDataHolder.
20-
func (c *CandidateRouteSearchDataHolderMock) SetCandidateRouteSearchData(candidateRouteSearchData map[string]domain.CandidateRouteDenomData) {
21+
func (c *CandidateRouteSearchDataHolderMock) SetCandidateRouteSearchData(candidateRouteSearchData map[string]*domain.CandidateRouteDenomData) {
2122
c.CandidateRouteSearchData = candidateRouteSearchData
2223
}
2324

2425
// GetDenomData implements mvc.CandidateRouteSearchDataHolder.
25-
func (c *CandidateRouteSearchDataHolderMock) GetDenomData(denom string) (domain.CandidateRouteDenomData, error) {
26+
func (c *CandidateRouteSearchDataHolderMock) GetDenomData(denom string) (*domain.CandidateRouteDenomData, error) {
2627
denomData, ok := c.CandidateRouteSearchData[denom]
2728
if !ok {
28-
return domain.CandidateRouteDenomData{}, nil
29+
return &domain.CandidateRouteDenomData{}, nil
2930
}
3031
return denomData, nil
3132
}

domain/mvc/router.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ import (
1414
// CandidateRouteSearchDataUpdateListener is the interface for the candidate route search data holder.
1515
type CandidateRouteSearchDataHolder interface {
1616
// SetCandidateRouteSearchData sets the candidate route search data on the holder
17-
SetCandidateRouteSearchData(candidateRouteSearchData map[string]domain.CandidateRouteDenomData)
17+
SetCandidateRouteSearchData(candidateRouteSearchData map[string]*domain.CandidateRouteDenomData)
1818

1919
// GetCandidateRouteSearchData gets the candidate route search data from the holder
20-
GetCandidateRouteSearchData() map[string]domain.CandidateRouteDenomData
20+
GetCandidateRouteSearchData() (map[string]*domain.CandidateRouteDenomData, error)
2121

2222
// GetDenomData returns the ranked candidate route search pool data for a given denom.
2323
// Returns an empty struct if the denom is not found.
2424
// Returns error if retrieved pools are not of type ingesttypes.PoolI.
25-
GetDenomData(denom string) (domain.CandidateRouteDenomData, error)
25+
GetDenomData(denom string) (*domain.CandidateRouteDenomData, error)
2626
}
2727

2828
// RouterRepository represents the contract for a repository handling tokens information

domain/osmomath/int.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package osmomath
2+
3+
import "github.com/osmosis-labs/osmosis/osmomath"
4+
5+
// SafeUint64 converts an osmomath.Int to a uint64.
6+
// It returns 0 if the input is nil, zero, or negative.
7+
// If the input is greater than uint64 max value, it returns max uint64 value.
8+
func SafeUint64(i osmomath.Int) uint64 {
9+
if i.IsNil() || i.IsZero() || i.IsNegative() {
10+
return 0
11+
}
12+
13+
if !i.IsUint64() {
14+
return ^uint64(0)
15+
}
16+
17+
return i.Uint64()
18+
}

domain/osmomath/int_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package osmomath_test
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
7+
"github.com/osmosis-labs/osmosis/osmomath"
8+
sqsosmomath "github.com/osmosis-labs/sqs/domain/osmomath"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestSafeUint64(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
input osmomath.Int
16+
expected uint64
17+
}{
18+
{
19+
name: "Nil input",
20+
input: osmomath.Int{},
21+
expected: 0,
22+
},
23+
{
24+
name: "Zero input",
25+
input: osmomath.NewInt(0),
26+
expected: 0,
27+
},
28+
{
29+
name: "Negative input",
30+
input: osmomath.NewInt(-5),
31+
expected: 0,
32+
},
33+
{
34+
name: "Positive input within uint64 range",
35+
input: osmomath.NewInt(42),
36+
expected: 42,
37+
},
38+
{
39+
name: "Maximum uint64 value",
40+
input: osmomath.NewIntFromUint64(^uint64(0)),
41+
expected: ^uint64(0),
42+
},
43+
{
44+
name: "Value exceeding uint64 range",
45+
input: osmomath.NewIntFromBigInt(new(big.Int).Add(big.NewInt(0).SetUint64(^uint64(0)), big.NewInt(1))),
46+
expected: ^uint64(0),
47+
},
48+
}
49+
50+
for _, tt := range tests {
51+
t.Run(tt.name, func(t *testing.T) {
52+
result := sqsosmomath.SafeUint64(tt.input)
53+
assert.Equal(t, tt.expected, result)
54+
})
55+
}
56+
}

domain/router.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ type RouterState struct {
141141
TakerFees ingesttypes.TakerFeeMap
142142
TickMap map[uint64]*ingesttypes.TickModel
143143
AlloyedDataMap map[uint64]*cosmwasmpool.AlloyTransmuterData
144-
CandidateRouteSearchData map[string]CandidateRouteDenomData
144+
CandidateRouteSearchData map[string]*CandidateRouteDenomData
145145
}
146146

147147
// RouterOptions defines the options for the router

domain/telemetry.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ var (
151151
// counter that measures the number of pricing coingecko cache misses
152152
SQSPricingCoingeckoCacheMissesCounterMetricName = "sqs_pricing_coingecko_cache_misses_total"
153153

154+
// sqs_pool_liq_pricing_worker_compute_duration
155+
//
156+
// gauge that tracks duration of candidate routes computation
157+
SQSCandidateRoutesComputeDurationMetricName = "sqs_candidate_routes_compute_duration"
158+
154159
SQSIngestHandlerProcessBlockHeightGauge = prometheus.NewGauge(
155160
prometheus.GaugeOpts{
156161
Name: SQSIngestUsecaseProcessBlockHeightMetricName,
@@ -305,6 +310,13 @@ var (
305310
Help: "Total number of pricing coingecko cache misses",
306311
},
307312
)
313+
314+
SQSCandidateRoutesComputeDurationGauge = prometheus.NewGauge(
315+
prometheus.GaugeOpts{
316+
Name: SQSCandidateRoutesComputeDurationMetricName,
317+
Help: "gauge that tracks duration of candidate routes computation",
318+
},
319+
)
308320
)
309321

310322
func init() {
@@ -330,4 +342,5 @@ func init() {
330342
prometheus.MustRegister(SQSPricingSpotPriceError)
331343
prometheus.MustRegister(SQSPricingCoingeckoCacheHitsCounter)
332344
prometheus.MustRegister(SQSPricingCoingeckoCacheMissesCounter)
345+
prometheus.MustRegister(SQSCandidateRoutesComputeDurationGauge)
333346
}

0 commit comments

Comments
 (0)