From 06eebaef75604ff8c6b05cb04fc11e2fa7b7f69b Mon Sep 17 00:00:00 2001 From: tamirms Date: Wed, 24 Nov 2021 10:14:43 +0000 Subject: [PATCH 1/3] Represent assets in orderbook graph as int32 instead of strings --- exp/orderbook/edges.go | 14 +- exp/orderbook/graph.go | 149 ++-- exp/orderbook/graph_benchmark_test.go | 21 +- exp/orderbook/graph_test.go | 744 ++++++++++-------- exp/orderbook/pools.go | 15 +- exp/orderbook/pools_test.go | 35 +- exp/orderbook/search.go | 259 +++--- exp/orderbook/utils.go | 4 +- services/horizon/internal/paths/main.go | 8 +- .../horizon/internal/resourceadapter/path.go | 26 +- 10 files changed, 715 insertions(+), 560 deletions(-) diff --git a/exp/orderbook/edges.go b/exp/orderbook/edges.go index fd8970814e..44a90f45fe 100644 --- a/exp/orderbook/edges.go +++ b/exp/orderbook/edges.go @@ -6,17 +6,17 @@ import ( "github.com/stellar/go/xdr" ) -// edgeSet maintains a mapping of strings (asset keys) to a set of venues, which +// edgeSet maintains a mapping of assets to a set of venues, which // is composed of a sorted lists of offers and, optionally, a liquidity pool. // The offers are sorted by ascending price (in terms of the buying asset). type edgeSet []edge type edge struct { - key string + key int32 value Venues } -func (e edgeSet) find(key string) int { +func (e edgeSet) find(key int32) int { for i := 0; i < len(e); i++ { if e[i].key == key { return i @@ -26,7 +26,7 @@ func (e edgeSet) find(key string) int { } // addOffer will insert the given offer into the edge set -func (e edgeSet) addOffer(key string, offer xdr.OfferEntry) edgeSet { +func (e edgeSet) addOffer(key int32, offer xdr.OfferEntry) edgeSet { // The list of offers in a venue is sorted by cheapest to most expensive // price to convert buyingAsset to sellingAsset i := e.find(key) @@ -51,7 +51,7 @@ func (e edgeSet) addOffer(key string, offer xdr.OfferEntry) edgeSet { } // addPool makes `pool` a viable venue at `key`. -func (e edgeSet) addPool(key string, pool xdr.LiquidityPoolEntry) edgeSet { +func (e edgeSet) addPool(key int32, pool liquidityPool) edgeSet { i := e.find(key) if i < 0 { return append(e, edge{key: key, value: Venues{pool: pool}}) @@ -62,7 +62,7 @@ func (e edgeSet) addPool(key string, pool xdr.LiquidityPoolEntry) edgeSet { // removeOffer will delete the given offer from the edge set, returning whether // or not the given offer was actually found. -func (e edgeSet) removeOffer(key string, offerID xdr.Int64) (edgeSet, bool) { +func (e edgeSet) removeOffer(key int32, offerID xdr.Int64) (edgeSet, bool) { i := e.find(key) if i < 0 { return e, false @@ -94,7 +94,7 @@ func (e edgeSet) removeOffer(key string, offerID xdr.Int64) (edgeSet, bool) { return e, true } -func (e edgeSet) removePool(key string) edgeSet { +func (e edgeSet) removePool(key int32) edgeSet { i := e.find(key) if i < 0 { return e diff --git a/exp/orderbook/graph.go b/exp/orderbook/graph.go index 556a931166..8ae6866765 100644 --- a/exp/orderbook/graph.go +++ b/exp/orderbook/graph.go @@ -28,9 +28,9 @@ const ( // trading pair represents two assets that can be exchanged if an order is fulfilled type tradingPair struct { // buyingAsset corresponds to offer.Buying.String() from an xdr.OfferEntry - buyingAsset string + buyingAsset int32 // sellingAsset corresponds to offer.Selling.String() from an xdr.OfferEntry - sellingAsset string + sellingAsset int32 } // OBGraph is an interface for orderbook graphs @@ -49,12 +49,16 @@ type OBGraph interface { // OrderBookGraph is an in-memory graph representation of all the offers in the // Stellar ledger. type OrderBookGraph struct { + assetStringToID map[string]int32 + idToAssetString []string + vacantIDs []int32 + // venuesForBuyingAsset maps an asset to all of its buying opportunities, // which may be offers (sorted by price) or a liquidity pools. - venuesForBuyingAsset map[string]edgeSet - // venuesForBuyingAsset maps an asset to all of its *selling* opportunities, + venuesForBuyingAsset []edgeSet + // venuesForSellingAsset maps an asset to all of its *selling* opportunities, // which may be offers (sorted by price) or a liquidity pools. - venuesForSellingAsset map[string]edgeSet + venuesForSellingAsset []edgeSet // liquidityPools associates a particular asset pair (in "asset order", see // xdr.Asset.LessThan) with a liquidity pool. liquidityPools map[tradingPair]xdr.LiquidityPoolEntry @@ -169,8 +173,11 @@ func (graph *OrderBookGraph) Clear() { graph.lock.Lock() defer graph.lock.Unlock() - graph.venuesForBuyingAsset = map[string]edgeSet{} - graph.venuesForSellingAsset = map[string]edgeSet{} + graph.assetStringToID = map[string]int32{} + graph.idToAssetString = []string{} + graph.vacantIDs = []int32{} + graph.venuesForSellingAsset = []edgeSet{} + graph.venuesForBuyingAsset = []edgeSet{} graph.tradingPairForOffer = map[xdr.Int64]tradingPair{} graph.liquidityPools = map[tradingPair]xdr.LiquidityPoolEntry{} graph.batchedUpdates = graph.batch() @@ -187,6 +194,38 @@ func (graph *OrderBookGraph) batch() *orderBookBatchedUpdates { } } +func (graph *OrderBookGraph) getOrCreateAssetID(asset xdr.Asset) int32 { + assetString := asset.String() + id, ok := graph.assetStringToID[assetString] + if ok { + return id + } + if len(graph.vacantIDs) > 0 { + id = graph.vacantIDs[len(graph.vacantIDs)-1] + graph.vacantIDs = graph.vacantIDs[:len(graph.vacantIDs)-1] + graph.idToAssetString[id] = assetString + } else { + id = int32(len(graph.idToAssetString)) + graph.idToAssetString = append(graph.idToAssetString, assetString) + graph.venuesForBuyingAsset = append(graph.venuesForBuyingAsset, nil) + graph.venuesForSellingAsset = append(graph.venuesForSellingAsset, nil) + } + + graph.assetStringToID[assetString] = id + return id +} + +func (graph *OrderBookGraph) maybeDeleteAsset(asset int32) { + buyingEdgesEmpty := len(graph.venuesForBuyingAsset[asset]) == 0 + sellingEdgesEmpty := len(graph.venuesForSellingAsset[asset]) == 0 + + if buyingEdgesEmpty && sellingEdgesEmpty { + delete(graph.assetStringToID, graph.idToAssetString[asset]) + graph.idToAssetString[asset] = "" + graph.vacantIDs = append(graph.vacantIDs, asset) + } +} + // addOffer inserts a given offer into the order book graph func (graph *OrderBookGraph) addOffer(offer xdr.OfferEntry) error { // If necessary, replace any existing offer with a new one. @@ -196,7 +235,8 @@ func (graph *OrderBookGraph) addOffer(offer xdr.OfferEntry) error { } } - buying, selling := offer.Buying.String(), offer.Selling.String() + buying := graph.getOrCreateAssetID(offer.Buying) + selling := graph.getOrCreateAssetID(offer.Selling) graph.tradingPairForOffer[offer.OfferId] = tradingPair{ buyingAsset: buying, sellingAsset: selling, @@ -208,19 +248,32 @@ func (graph *OrderBookGraph) addOffer(offer xdr.OfferEntry) error { return nil } +func (graph *OrderBookGraph) poolFromEntry(poolXDR xdr.LiquidityPoolEntry) liquidityPool { + aXDR, bXDR := getPoolAssets(poolXDR) + assetA, assetB := graph.getOrCreateAssetID(aXDR), graph.getOrCreateAssetID(bXDR) + return liquidityPool{ + LiquidityPoolEntry: poolXDR, + assetA: assetA, + assetB: assetB, + } +} + // addPool sets the given pool as the venue for the given trading pair. -func (graph *OrderBookGraph) addPool(pool xdr.LiquidityPoolEntry) { +func (graph *OrderBookGraph) addPool(poolEntry xdr.LiquidityPoolEntry) { // Liquidity pools have no concept of a "buying" or "selling" asset, // so we create venues in both directions. - x, y := getPoolAssets(pool) - graph.liquidityPools[tradingPair{x, y}] = pool + pool := graph.poolFromEntry(poolEntry) + graph.liquidityPools[tradingPair{ + buyingAsset: pool.assetA, + sellingAsset: pool.assetB, + }] = pool.LiquidityPoolEntry - for _, table := range []map[string]edgeSet{ + for _, table := range [][]edgeSet{ graph.venuesForBuyingAsset, graph.venuesForSellingAsset, } { - table[x] = table[x].addPool(y, pool) - table[y] = table[y].addPool(x, pool) + table[pool.assetA] = table[pool.assetA].addPool(pool.assetB, pool) + table[pool.assetB] = table[pool.assetB].addPool(pool.assetA, pool) } } @@ -230,43 +283,37 @@ func (graph *OrderBookGraph) removeOffer(offerID xdr.Int64) error { if !ok { return errOfferNotPresent } - delete(graph.tradingPairForOffer, offerID) - if set, ok := graph.venuesForSellingAsset[pair.sellingAsset]; !ok { - return errOfferNotPresent - } else if set, ok = set.removeOffer(pair.buyingAsset, offerID); !ok { + if set, ok := graph.venuesForSellingAsset[pair.sellingAsset].removeOffer(pair.buyingAsset, offerID); !ok { return errOfferNotPresent - } else if len(set) == 0 { - delete(graph.venuesForSellingAsset, pair.sellingAsset) } else { graph.venuesForSellingAsset[pair.sellingAsset] = set } - if set, ok := graph.venuesForBuyingAsset[pair.buyingAsset]; !ok { + if set, ok := graph.venuesForBuyingAsset[pair.buyingAsset].removeOffer(pair.sellingAsset, offerID); !ok { return errOfferNotPresent - } else if set, ok = set.removeOffer(pair.sellingAsset, offerID); !ok { - return errOfferNotPresent - } else if len(set) == 0 { - delete(graph.venuesForBuyingAsset, pair.buyingAsset) } else { graph.venuesForBuyingAsset[pair.buyingAsset] = set } + graph.maybeDeleteAsset(pair.buyingAsset) + graph.maybeDeleteAsset(pair.sellingAsset) return nil } // removePool unsets the pool matching the given asset pair, if it exists. -func (graph *OrderBookGraph) removePool(pool xdr.LiquidityPoolEntry) { - x, y := getPoolAssets(pool) - - for _, asset := range []string{x, y} { - otherAsset := x - if asset == x { - otherAsset = y +func (graph *OrderBookGraph) removePool(poolXDR xdr.LiquidityPoolEntry) { + aXDR, bXDR := getPoolAssets(poolXDR) + assetA, assetB := graph.getOrCreateAssetID(aXDR), graph.getOrCreateAssetID(bXDR) + + for _, asset := range []int32{assetA, assetB} { + otherAsset := assetB + if asset == assetB { + otherAsset = assetA } - for _, table := range []map[string]edgeSet{ + for _, table := range [][]edgeSet{ graph.venuesForBuyingAsset, graph.venuesForSellingAsset, } { @@ -274,7 +321,9 @@ func (graph *OrderBookGraph) removePool(pool xdr.LiquidityPoolEntry) { } } - delete(graph.liquidityPools, tradingPair{x, y}) + delete(graph.liquidityPools, tradingPair{assetA, assetB}) + graph.maybeDeleteAsset(assetA) + graph.maybeDeleteAsset(assetB) } // IsEmpty returns true if the orderbook graph is not populated @@ -282,7 +331,7 @@ func (graph *OrderBookGraph) IsEmpty() bool { graph.lock.RLock() defer graph.lock.RUnlock() - return len(graph.venuesForSellingAsset) == 0 + return len(graph.liquidityPools) == 0 && len(graph.tradingPairForOffer) == 0 } // FindPaths returns a list of payment paths originating from a source account @@ -300,15 +349,15 @@ func (graph *OrderBookGraph) FindPaths( includePools bool, ) ([]Path, uint32, error) { destinationAssetString := destinationAsset.String() - sourceAssetsMap := make(map[string]xdr.Int64, len(sourceAssets)) + sourceAssetsMap := make(map[int32]xdr.Int64, len(sourceAssets)) for i, sourceAsset := range sourceAssets { sourceAssetString := sourceAsset.String() - sourceAssetsMap[sourceAssetString] = sourceAssetBalances[i] + sourceAssetsMap[graph.assetStringToID[sourceAssetString]] = sourceAssetBalances[i] } searchState := &sellingGraphSearchState{ graph: graph, - destinationAsset: destinationAsset, + destinationAssetString: destinationAssetString, destinationAssetAmount: destinationAmount, ignoreOffersFrom: sourceAccountID, targetAssets: sourceAssetsMap, @@ -321,8 +370,7 @@ func (graph *OrderBookGraph) FindPaths( ctx, searchState, maxPathLength, - destinationAssetString, - destinationAsset, + graph.assetStringToID[destinationAssetString], destinationAmount, ) lastLedger := graph.lastLedger @@ -356,15 +404,15 @@ func (graph *OrderBookGraph) FindFixedPaths( maxAssetsPerPath int, includePools bool, ) ([]Path, uint32, error) { - target := map[string]bool{} + target := map[int32]bool{} for _, destinationAsset := range destinationAssets { destinationAssetString := destinationAsset.String() - target[destinationAssetString] = true + target[graph.assetStringToID[destinationAssetString]] = true } searchState := &buyingGraphSearchState{ graph: graph, - sourceAsset: sourceAsset, + sourceAssetString: sourceAsset.String(), sourceAssetAmount: amountToSpend, targetAssets: target, paths: []Path{}, @@ -375,8 +423,7 @@ func (graph *OrderBookGraph) FindFixedPaths( ctx, searchState, maxPathLength, - sourceAsset.String(), - sourceAsset, + graph.assetStringToID[sourceAsset.String()], amountToSpend, ) lastLedger := graph.lastLedger @@ -402,13 +449,13 @@ func (graph *OrderBookGraph) FindFixedPaths( // if there are multiple paths which spend the same `SourceAmount` then shorter payment paths // will be prioritized func compareSourceAsset(allPaths []Path, i, j int) bool { - if allPaths[i].SourceAsset.Equals(allPaths[j].SourceAsset) { + if allPaths[i].SourceAsset == allPaths[j].SourceAsset { if allPaths[i].SourceAmount == allPaths[j].SourceAmount { return len(allPaths[i].InteriorNodes) < len(allPaths[j].InteriorNodes) } return allPaths[i].SourceAmount < allPaths[j].SourceAmount } - return allPaths[i].SourceAssetString() < allPaths[j].SourceAssetString() + return allPaths[i].SourceAsset < allPaths[j].SourceAsset } // compareDestinationAsset will group payment paths by `DestinationAsset`. Paths @@ -416,21 +463,21 @@ func compareSourceAsset(allPaths []Path, i, j int) bool { // sorting. If there are multiple paths which deliver the same // `DestinationAmount`, then shorter payment paths will be prioritized. func compareDestinationAsset(allPaths []Path, i, j int) bool { - if allPaths[i].DestinationAsset.Equals(allPaths[j].DestinationAsset) { + if allPaths[i].DestinationAsset == allPaths[j].DestinationAsset { if allPaths[i].DestinationAmount == allPaths[j].DestinationAmount { return len(allPaths[i].InteriorNodes) < len(allPaths[j].InteriorNodes) } return allPaths[i].DestinationAmount > allPaths[j].DestinationAmount } - return allPaths[i].DestinationAssetString() < allPaths[j].DestinationAssetString() + return allPaths[i].DestinationAsset < allPaths[j].DestinationAsset } func sourceAssetEquals(p, otherPath Path) bool { - return p.SourceAsset.Equals(otherPath.SourceAsset) + return p.SourceAsset == otherPath.SourceAsset } func destinationAssetEquals(p, otherPath Path) bool { - return p.DestinationAsset.Equals(otherPath.DestinationAsset) + return p.DestinationAsset == otherPath.DestinationAsset } // sortAndFilterPaths sorts the given list of paths using `comparePaths` diff --git a/exp/orderbook/graph_benchmark_test.go b/exp/orderbook/graph_benchmark_test.go index 599f4aacc5..7d5a260522 100644 --- a/exp/orderbook/graph_benchmark_test.go +++ b/exp/orderbook/graph_benchmark_test.go @@ -219,11 +219,18 @@ func BenchmarkTestData(b *testing.B) { } func BenchmarkSingleLiquidityPoolExchange(b *testing.B) { + graph := NewOrderBookGraph() + pool := graph.poolFromEntry(eurUsdLiquidityPool) + asset := graph.getOrCreateAssetID(usdAsset) + + b.ResetTimer() + b.ReportAllocs() + b.Run("deposit", func(b *testing.B) { - makeTrade(eurUsdLiquidityPool, usdAsset, tradeTypeDeposit, math.MaxInt64/2) + makeTrade(pool, asset, tradeTypeDeposit, math.MaxInt64/2) }) b.Run("exchange", func(b *testing.B) { - makeTrade(eurUsdLiquidityPool, usdAsset, tradeTypeExpectation, math.MaxInt64/2) + makeTrade(pool, asset, tradeTypeExpectation, math.MaxInt64/2) }) } @@ -232,23 +239,29 @@ func BenchmarkSingleLiquidityPoolExchange(b *testing.B) { func BenchmarkLiquidityPoolDeposits(b *testing.B) { amounts := createRandomAmounts(b.N) + graph := NewOrderBookGraph() + pool := graph.poolFromEntry(eurUsdLiquidityPool) + asset := graph.getOrCreateAssetID(usdAsset) b.ResetTimer() b.ReportAllocs() for _, amount := range amounts { - makeTrade(eurUsdLiquidityPool, usdAsset, tradeTypeDeposit, amount) + makeTrade(pool, asset, tradeTypeDeposit, amount) } } func BenchmarkLiquidityPoolExpectations(b *testing.B) { amounts := createRandomAmounts(b.N) + graph := NewOrderBookGraph() + pool := graph.poolFromEntry(eurUsdLiquidityPool) + asset := graph.getOrCreateAssetID(usdAsset) b.ResetTimer() b.ReportAllocs() for _, amount := range amounts { - makeTrade(eurUsdLiquidityPool, usdAsset, tradeTypeExpectation, amount) + makeTrade(pool, asset, tradeTypeExpectation, amount) } } diff --git a/exp/orderbook/graph_test.go b/exp/orderbook/graph_test.go index 356e9cefe2..d985a075cd 100644 --- a/exp/orderbook/graph_test.go +++ b/exp/orderbook/graph_test.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding" - "fmt" "math" "sort" "testing" @@ -157,49 +156,84 @@ func assertOfferListEquals(t *testing.T, a, b []xdr.OfferEntry) { // assertGraphEquals ensures two graphs are identical func assertGraphEquals(t *testing.T, a, b *OrderBookGraph) { - assert.Equalf(t, len(a.venuesForBuyingAsset), len(b.venuesForBuyingAsset), - "expected same # of buying venues but got %v %v", - a.venuesForBuyingAsset, b.venuesForBuyingAsset) - - assert.Equalf(t, len(a.venuesForSellingAsset), len(b.venuesForSellingAsset), - "expected same # of selling venues but got %v %v", - a.venuesForSellingAsset, b.venuesForSellingAsset) + assert.Equalf(t, len(a.assetStringToID), len(b.assetStringToID), + "expected same # of asset string to id entries but got %v %v", + a.assetStringToID, b.assetStringToID) assert.Equalf(t, len(a.tradingPairForOffer), len(b.tradingPairForOffer), "expected same # of trading pairs but got %v %v", a, b) - for sellingAsset, edgeSet := range a.venuesForSellingAsset { - otherEdgeSet := b.venuesForSellingAsset[sellingAsset] + assert.Equalf(t, len(a.liquidityPools), len(b.liquidityPools), + "expected same # of liquidity pools but got %v %v", a, b) - assert.Equalf(t, len(edgeSet), len(otherEdgeSet), - "expected edge set for %v to have same length but got %v %v", - sellingAsset, edgeSet, otherEdgeSet) + for assetString, _ := range a.assetStringToID { + asset := a.assetStringToID[assetString] + otherAsset, ok := b.assetStringToID[assetString] + if !ok { + t.Fatalf("asset %v is not present in assetStringToID", assetString) + } + es := a.venuesForSellingAsset[asset] + other := b.venuesForSellingAsset[otherAsset] - for _, edge := range edgeSet { - venues := edge.value - otherVenues := findByAsset(otherEdgeSet, edge.key) + assertEdgeSetEquals(t, a, b, es, other, assetString) - assert.Equalf(t, venues.pool, otherVenues.pool, - "expected pools for %v to be equal") + es = a.venuesForBuyingAsset[asset] + other = b.venuesForBuyingAsset[otherAsset] - assert.Equalf(t, len(venues.offers), len(otherVenues.offers), - "expected offers for %v to have same length but got %v %v", - edge.key, venues.offers, otherVenues.offers, - ) + assert.Equalf(t, len(es), len(other), + "expected edge set for %v to have same length but got %v %v", + assetString, es, other) - assertOfferListEquals(t, venues.offers, otherVenues.offers) - } + assertEdgeSetEquals(t, a, b, es, other, assetString) } for offerID, pair := range a.tradingPairForOffer { otherPair := b.tradingPairForOffer[offerID] - assert.Equalf(t, pair.buyingAsset, otherPair.buyingAsset, + assert.Equalf( + t, + a.idToAssetString[pair.buyingAsset], + b.idToAssetString[otherPair.buyingAsset], "expected trading pair to match but got %v %v", pair, otherPair) - assert.Equalf(t, pair.sellingAsset, otherPair.sellingAsset, + assert.Equalf( + t, + a.idToAssetString[pair.sellingAsset], + b.idToAssetString[otherPair.sellingAsset], "expected trading pair to match but got %v %v", pair, otherPair) } + + for pair, pool := range a.liquidityPools { + otherPair := tradingPair{ + buyingAsset: b.assetStringToID[a.idToAssetString[pair.buyingAsset]], + sellingAsset: b.assetStringToID[a.idToAssetString[pair.sellingAsset]], + } + otherPool := b.liquidityPools[otherPair] + assert.Equalf(t, pool, otherPool, "expected pool to match but got %v %v", pool, otherPool) + } +} + +func assertEdgeSetEquals( + t *testing.T, a *OrderBookGraph, b *OrderBookGraph, + es edgeSet, other edgeSet, assetString string) { + assert.Equalf(t, len(es), len(other), + "expected edge set for %v to have same length but got %v %v", + assetString, es, other) + + for _, edge := range es { + venues := edge.value + otherVenues := findByAsset(b, other, a.idToAssetString[edge.key]) + + assert.Equalf(t, venues.pool.LiquidityPoolEntry, otherVenues.pool.LiquidityPoolEntry, + "expected pools for %v to be equal") + + assert.Equalf(t, len(venues.offers), len(otherVenues.offers), + "expected offers for %v to have same length but got %v %v", + edge.key, venues.offers, otherVenues.offers, + ) + + assertOfferListEquals(t, venues.offers, otherVenues.offers) + } } func assertPathEquals(t *testing.T, a, b []Path) { @@ -215,24 +249,28 @@ func assertPathEquals(t *testing.T, a, b []Path) { assert.Equalf(t, a[i].DestinationAmount, b[i].DestinationAmount, "expected dest amounts to be same got %v %v", a[i], b[i]) - assert.Truef(t, a[i].DestinationAsset.Equals(b[i].DestinationAsset), + assert.Equalf(t, a[i].DestinationAsset, b[i].DestinationAsset, "expected dest assets to be same got %v %v", a[i], b[i]) - assert.Truef(t, a[i].SourceAsset.Equals(b[i].SourceAsset), + assert.Equalf(t, a[i].SourceAsset, b[i].SourceAsset, "expected source assets to be same got %v %v", a[i], b[i]) assert.Equalf(t, len(a[i].InteriorNodes), len(b[i].InteriorNodes), "expected interior nodes have same length got %v %v", a[i], b[i]) for j := 0; j > len(a[i].InteriorNodes); j++ { - assert.Truef(t, - a[i].InteriorNodes[j].Equals(b[i].InteriorNodes[j]), + assert.Equalf(t, + a[i].InteriorNodes[j], b[i].InteriorNodes[j], "expected interior nodes to be same got %v %v", a[i], b[i]) } } } -func findByAsset(edges edgeSet, asset string) Venues { +func findByAsset(g *OrderBookGraph, edges edgeSet, assetString string) Venues { + asset, ok := g.assetStringToID[assetString] + if !ok { + return Venues{} + } i := edges.find(asset) if i >= 0 { return edges[i].value @@ -242,27 +280,28 @@ func findByAsset(edges edgeSet, asset string) Venues { func TestAddEdgeSet(t *testing.T) { set := edgeSet{} + g := NewOrderBookGraph() - set = set.addOffer(dollarOffer.Buying.String(), dollarOffer) - set = set.addOffer(eurOffer.Buying.String(), eurOffer) - set = set.addOffer(twoEurOffer.Buying.String(), twoEurOffer) - set = set.addOffer(threeEurOffer.Buying.String(), threeEurOffer) - set = set.addOffer(quarterOffer.Buying.String(), quarterOffer) - set = set.addOffer(fiftyCentsOffer.Buying.String(), fiftyCentsOffer) - set = set.addPool(usdAsset.String(), eurUsdLiquidityPool) - set = set.addPool(eurAsset.String(), eurUsdLiquidityPool) + set = set.addOffer(g.getOrCreateAssetID(dollarOffer.Buying), dollarOffer) + set = set.addOffer(g.getOrCreateAssetID(eurOffer.Buying), eurOffer) + set = set.addOffer(g.getOrCreateAssetID(twoEurOffer.Buying), twoEurOffer) + set = set.addOffer(g.getOrCreateAssetID(threeEurOffer.Buying), threeEurOffer) + set = set.addOffer(g.getOrCreateAssetID(quarterOffer.Buying), quarterOffer) + set = set.addOffer(g.getOrCreateAssetID(fiftyCentsOffer.Buying), fiftyCentsOffer) + set = set.addPool(g.getOrCreateAssetID(usdAsset), g.poolFromEntry(eurUsdLiquidityPool)) + set = set.addPool(g.getOrCreateAssetID(eurAsset), g.poolFromEntry(eurUsdLiquidityPool)) assert.Lenf(t, set, 2, "expected set to have 2 entries but got %v", set) - assert.Equal(t, findByAsset(set, usdAsset.String()).pool, eurUsdLiquidityPool) - assert.Equal(t, findByAsset(set, eurAsset.String()).pool, eurUsdLiquidityPool) + assert.Equal(t, findByAsset(g, set, usdAsset.String()).pool.LiquidityPoolEntry, eurUsdLiquidityPool) + assert.Equal(t, findByAsset(g, set, eurAsset.String()).pool.LiquidityPoolEntry, eurUsdLiquidityPool) - assertOfferListEquals(t, findByAsset(set, usdAsset.String()).offers, []xdr.OfferEntry{ + assertOfferListEquals(t, findByAsset(g, set, usdAsset.String()).offers, []xdr.OfferEntry{ quarterOffer, fiftyCentsOffer, dollarOffer, }) - assertOfferListEquals(t, findByAsset(set, eurAsset.String()).offers, []xdr.OfferEntry{ + assertOfferListEquals(t, findByAsset(g, set, eurAsset.String()).offers, []xdr.OfferEntry{ eurOffer, twoEurOffer, threeEurOffer, @@ -271,38 +310,39 @@ func TestAddEdgeSet(t *testing.T) { func TestRemoveEdgeSet(t *testing.T) { set := edgeSet{} + g := NewOrderBookGraph() var found bool - set, found = set.removeOffer(usdAsset.String(), dollarOffer.OfferId) + set, found = set.removeOffer(g.getOrCreateAssetID(usdAsset), dollarOffer.OfferId) assert.Falsef(t, found, "expected set to not contain asset but is %v", set) - set = set.addOffer(dollarOffer.Buying.String(), dollarOffer) - set = set.addOffer(eurOffer.Buying.String(), eurOffer) - set = set.addOffer(twoEurOffer.Buying.String(), twoEurOffer) - set = set.addOffer(threeEurOffer.Buying.String(), threeEurOffer) - set = set.addOffer(quarterOffer.Buying.String(), quarterOffer) - set = set.addOffer(fiftyCentsOffer.Buying.String(), fiftyCentsOffer) - set = set.addPool(usdAsset.String(), eurUsdLiquidityPool) + set = set.addOffer(g.getOrCreateAssetID(dollarOffer.Buying), dollarOffer) + set = set.addOffer(g.getOrCreateAssetID(eurOffer.Buying), eurOffer) + set = set.addOffer(g.getOrCreateAssetID(twoEurOffer.Buying), twoEurOffer) + set = set.addOffer(g.getOrCreateAssetID(threeEurOffer.Buying), threeEurOffer) + set = set.addOffer(g.getOrCreateAssetID(quarterOffer.Buying), quarterOffer) + set = set.addOffer(g.getOrCreateAssetID(fiftyCentsOffer.Buying), fiftyCentsOffer) + set = set.addPool(g.getOrCreateAssetID(usdAsset), g.poolFromEntry(eurUsdLiquidityPool)) - set = set.removePool(usdAsset.String()) - assert.Nil(t, findByAsset(set, usdAsset.String()).pool.Body.ConstantProduct) + set = set.removePool(g.getOrCreateAssetID(usdAsset)) + assert.Nil(t, findByAsset(g, set, usdAsset.String()).pool.Body.ConstantProduct) - set, found = set.removeOffer(usdAsset.String(), dollarOffer.OfferId) + set, found = set.removeOffer(g.getOrCreateAssetID(usdAsset), dollarOffer.OfferId) assert.Truef(t, found, "expected set to contain dollar offer but is %v", set) - set, found = set.removeOffer(usdAsset.String(), dollarOffer.OfferId) + set, found = set.removeOffer(g.getOrCreateAssetID(usdAsset), dollarOffer.OfferId) assert.Falsef(t, found, "expected set to not contain dollar offer after deletion but is %v", set) - set, found = set.removeOffer(eurAsset.String(), threeEurOffer.OfferId) + set, found = set.removeOffer(g.getOrCreateAssetID(eurAsset), threeEurOffer.OfferId) assert.Truef(t, found, "expected set to contain three euro offer but is %v", set) - set, found = set.removeOffer(eurAsset.String(), eurOffer.OfferId) + set, found = set.removeOffer(g.getOrCreateAssetID(eurAsset), eurOffer.OfferId) assert.Truef(t, found, "expected set to contain euro offer but is %v", set) - set, found = set.removeOffer(eurAsset.String(), twoEurOffer.OfferId) + set, found = set.removeOffer(g.getOrCreateAssetID(eurAsset), twoEurOffer.OfferId) assert.Truef(t, found, "expected set to contain two euro offer but is %v", set) - set, found = set.removeOffer(eurAsset.String(), eurOffer.OfferId) + set, found = set.removeOffer(g.getOrCreateAssetID(eurAsset), eurOffer.OfferId) assert.Falsef(t, found, "expected set to not contain euro offer after deletion but is %v", set) assert.Lenf(t, set, 1, "%v", set) - assertOfferListEquals(t, findByAsset(set, usdAsset.String()).offers, []xdr.OfferEntry{ + assertOfferListEquals(t, findByAsset(g, set, usdAsset.String()).offers, []xdr.OfferEntry{ quarterOffer, fiftyCentsOffer, }) @@ -404,63 +444,77 @@ func TestAddOffersOrderBook(t *testing.T) { t.FailNow() } + assetStringToID := map[string]int32{} + idToAssetString := []string{} + for i, asset := range []xdr.Asset{ + nativeAsset, + usdAsset, + eurAsset, + } { + assetStringToID[asset.String()] = int32(i) + idToAssetString = append(idToAssetString, asset.String()) + } + expectedGraph := &OrderBookGraph{ - venuesForSellingAsset: map[string]edgeSet{ - nativeAsset.String(): { + assetStringToID: assetStringToID, + idToAssetString: idToAssetString, + venuesForSellingAsset: []edgeSet{ + { { - usdAsset.String(), + assetStringToID[usdAsset.String()], makeVenues(quarterOffer, fiftyCentsOffer, dollarOffer), }, { - eurAsset.String(), + assetStringToID[eurAsset.String()], makeVenues(eurOffer, twoEurOffer, threeEurOffer), }, }, - usdAsset.String(): { + { { - eurAsset.String(), + assetStringToID[eurAsset.String()], makeVenues(eurUsdOffer, otherEurUsdOffer), }, }, - eurAsset.String(): { + { { - usdAsset.String(), + assetStringToID[usdAsset.String()], makeVenues(usdEurOffer), }, }, }, - venuesForBuyingAsset: map[string]edgeSet{ - usdAsset.String(): { + venuesForBuyingAsset: []edgeSet{ + {}, + { { - eurAsset.String(), + assetStringToID[eurAsset.String()], makeVenues(usdEurOffer), }, { - nativeAsset.String(), + assetStringToID[nativeAsset.String()], makeVenues(quarterOffer, fiftyCentsOffer, dollarOffer), }, }, - eurAsset.String(): { + { { - usdAsset.String(), + assetStringToID[usdAsset.String()], makeVenues(eurUsdOffer, otherEurUsdOffer), }, { - nativeAsset.String(), + assetStringToID[nativeAsset.String()], makeVenues(eurOffer, twoEurOffer, threeEurOffer), }, }, }, tradingPairForOffer: map[xdr.Int64]tradingPair{ - quarterOffer.OfferId: makeTradingPair(usdAsset, nativeAsset), - fiftyCentsOffer.OfferId: makeTradingPair(usdAsset, nativeAsset), - dollarOffer.OfferId: makeTradingPair(usdAsset, nativeAsset), - eurOffer.OfferId: makeTradingPair(eurAsset, nativeAsset), - twoEurOffer.OfferId: makeTradingPair(eurAsset, nativeAsset), - threeEurOffer.OfferId: makeTradingPair(eurAsset, nativeAsset), - eurUsdOffer.OfferId: makeTradingPair(eurAsset, usdAsset), - otherEurUsdOffer.OfferId: makeTradingPair(eurAsset, usdAsset), - usdEurOffer.OfferId: makeTradingPair(usdAsset, eurAsset), + quarterOffer.OfferId: makeTradingPair(assetStringToID, usdAsset, nativeAsset), + fiftyCentsOffer.OfferId: makeTradingPair(assetStringToID, usdAsset, nativeAsset), + dollarOffer.OfferId: makeTradingPair(assetStringToID, usdAsset, nativeAsset), + eurOffer.OfferId: makeTradingPair(assetStringToID, eurAsset, nativeAsset), + twoEurOffer.OfferId: makeTradingPair(assetStringToID, eurAsset, nativeAsset), + threeEurOffer.OfferId: makeTradingPair(assetStringToID, eurAsset, nativeAsset), + eurUsdOffer.OfferId: makeTradingPair(assetStringToID, eurAsset, usdAsset), + otherEurUsdOffer.OfferId: makeTradingPair(assetStringToID, eurAsset, usdAsset), + usdEurOffer.OfferId: makeTradingPair(assetStringToID, usdAsset, eurAsset), }, } @@ -633,63 +687,76 @@ func TestUpdateOfferOrderBook(t *testing.T) { t.Fatalf("expected last ledger to be %v but got %v", 3, graph.lastLedger) } + assetStringToID := map[string]int32{} + idToAssetString := []string{} + for i, asset := range []xdr.Asset{ + nativeAsset, + usdAsset, + eurAsset, + } { + assetStringToID[asset.String()] = int32(i) + idToAssetString = append(idToAssetString, asset.String()) + } expectedGraph := &OrderBookGraph{ - venuesForSellingAsset: map[string]edgeSet{ - nativeAsset.String(): { + idToAssetString: idToAssetString, + assetStringToID: assetStringToID, + venuesForSellingAsset: []edgeSet{ + { { - usdAsset.String(), + assetStringToID[usdAsset.String()], makeVenues(quarterOffer, fiftyCentsOffer, dollarOffer), }, { - eurAsset.String(), + assetStringToID[eurAsset.String()], makeVenues(eurOffer, twoEurOffer, threeEurOffer), }, }, - usdAsset.String(): { + { { - eurAsset.String(), + assetStringToID[eurAsset.String()], makeVenues(otherEurUsdOffer, eurUsdOffer), }, }, - eurAsset.String(): { + { { - usdAsset.String(), + assetStringToID[usdAsset.String()], makeVenues(usdEurOffer), }, }, }, - venuesForBuyingAsset: map[string]edgeSet{ - usdAsset.String(): { + venuesForBuyingAsset: []edgeSet{ + {}, + { { - nativeAsset.String(), + assetStringToID[nativeAsset.String()], makeVenues(quarterOffer, fiftyCentsOffer, dollarOffer), }, { - eurAsset.String(), + assetStringToID[eurAsset.String()], makeVenues(usdEurOffer), }, }, - eurAsset.String(): { + { { - nativeAsset.String(), + assetStringToID[nativeAsset.String()], makeVenues(eurOffer, twoEurOffer, threeEurOffer), }, { - usdAsset.String(), + assetStringToID[usdAsset.String()], makeVenues(otherEurUsdOffer, eurUsdOffer), }, }, }, tradingPairForOffer: map[xdr.Int64]tradingPair{ - quarterOffer.OfferId: makeTradingPair(usdAsset, nativeAsset), - fiftyCentsOffer.OfferId: makeTradingPair(usdAsset, nativeAsset), - dollarOffer.OfferId: makeTradingPair(usdAsset, nativeAsset), - eurOffer.OfferId: makeTradingPair(eurAsset, nativeAsset), - twoEurOffer.OfferId: makeTradingPair(eurAsset, nativeAsset), - threeEurOffer.OfferId: makeTradingPair(eurAsset, nativeAsset), - eurUsdOffer.OfferId: makeTradingPair(eurAsset, usdAsset), - otherEurUsdOffer.OfferId: makeTradingPair(eurAsset, usdAsset), - usdEurOffer.OfferId: makeTradingPair(usdAsset, eurAsset), + quarterOffer.OfferId: makeTradingPair(assetStringToID, usdAsset, nativeAsset), + fiftyCentsOffer.OfferId: makeTradingPair(assetStringToID, usdAsset, nativeAsset), + dollarOffer.OfferId: makeTradingPair(assetStringToID, usdAsset, nativeAsset), + eurOffer.OfferId: makeTradingPair(assetStringToID, eurAsset, nativeAsset), + twoEurOffer.OfferId: makeTradingPair(assetStringToID, eurAsset, nativeAsset), + threeEurOffer.OfferId: makeTradingPair(assetStringToID, eurAsset, nativeAsset), + eurUsdOffer.OfferId: makeTradingPair(assetStringToID, eurAsset, usdAsset), + otherEurUsdOffer.OfferId: makeTradingPair(assetStringToID, eurAsset, usdAsset), + usdEurOffer.OfferId: makeTradingPair(assetStringToID, usdAsset, eurAsset), }, } @@ -791,50 +858,64 @@ func TestRemoveOfferOrderBook(t *testing.T) { t.FailNow() } + assetStringToID := map[string]int32{} + idToAssetString := []string{} + for i, asset := range []xdr.Asset{ + nativeAsset, + usdAsset, + eurAsset, + } { + assetStringToID[asset.String()] = int32(i) + idToAssetString = append(idToAssetString, asset.String()) + } expectedGraph := &OrderBookGraph{ - venuesForSellingAsset: map[string]edgeSet{ - nativeAsset.String(): { + idToAssetString: idToAssetString, + assetStringToID: assetStringToID, + venuesForSellingAsset: []edgeSet{ + { { - usdAsset.String(), + assetStringToID[usdAsset.String()], makeVenues(quarterOffer, fiftyCentsOffer), }, { - eurAsset.String(), + assetStringToID[eurAsset.String()], makeVenues(eurOffer, twoEurOffer, threeEurOffer), }, }, - usdAsset.String(): { + { { - eurAsset.String(), + assetStringToID[eurAsset.String()], makeVenues(eurUsdOffer), }, }, + {}, }, - venuesForBuyingAsset: map[string]edgeSet{ - usdAsset.String(): { + venuesForBuyingAsset: []edgeSet{ + {}, + { { - nativeAsset.String(), + assetStringToID[nativeAsset.String()], makeVenues(quarterOffer, fiftyCentsOffer), }, }, - eurAsset.String(): { + { { - nativeAsset.String(), + assetStringToID[nativeAsset.String()], makeVenues(eurOffer, twoEurOffer, threeEurOffer), }, { - usdAsset.String(), + assetStringToID[usdAsset.String()], makeVenues(eurUsdOffer), }, }, }, tradingPairForOffer: map[xdr.Int64]tradingPair{ - quarterOffer.OfferId: makeTradingPair(usdAsset, nativeAsset), - fiftyCentsOffer.OfferId: makeTradingPair(usdAsset, nativeAsset), - eurOffer.OfferId: makeTradingPair(eurAsset, nativeAsset), - twoEurOffer.OfferId: makeTradingPair(eurAsset, nativeAsset), - threeEurOffer.OfferId: makeTradingPair(eurAsset, nativeAsset), - eurUsdOffer.OfferId: makeTradingPair(eurAsset, usdAsset), + quarterOffer.OfferId: makeTradingPair(assetStringToID, usdAsset, nativeAsset), + fiftyCentsOffer.OfferId: makeTradingPair(assetStringToID, usdAsset, nativeAsset), + eurOffer.OfferId: makeTradingPair(assetStringToID, eurAsset, nativeAsset), + twoEurOffer.OfferId: makeTradingPair(assetStringToID, eurAsset, nativeAsset), + threeEurOffer.OfferId: makeTradingPair(assetStringToID, eurAsset, nativeAsset), + eurUsdOffer.OfferId: makeTradingPair(assetStringToID, eurAsset, usdAsset), }, } @@ -1084,52 +1165,46 @@ func TestSortAndFilterPathsBySourceAsset(t *testing.T) { allPaths := []Path{ { SourceAmount: 3, - SourceAsset: eurAsset, - sourceAssetString: eurAsset.String(), - InteriorNodes: []xdr.Asset{}, - DestinationAsset: yenAsset, + SourceAsset: eurAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: yenAsset.String(), DestinationAmount: 1000, }, { SourceAmount: 4, - SourceAsset: eurAsset, - sourceAssetString: eurAsset.String(), - InteriorNodes: []xdr.Asset{}, - DestinationAsset: yenAsset, + SourceAsset: eurAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: yenAsset.String(), DestinationAmount: 1000, }, { SourceAmount: 1, - SourceAsset: usdAsset, - sourceAssetString: usdAsset.String(), - InteriorNodes: []xdr.Asset{}, - DestinationAsset: yenAsset, + SourceAsset: usdAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: yenAsset.String(), DestinationAmount: 1000, }, { SourceAmount: 2, - SourceAsset: eurAsset, - sourceAssetString: eurAsset.String(), - InteriorNodes: []xdr.Asset{}, - DestinationAsset: yenAsset, + SourceAsset: eurAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: yenAsset.String(), DestinationAmount: 1000, }, { - SourceAmount: 2, - SourceAsset: eurAsset, - sourceAssetString: eurAsset.String(), - InteriorNodes: []xdr.Asset{ - nativeAsset, + SourceAmount: 2, + SourceAsset: eurAsset.String(), + InteriorNodes: []string{ + nativeAsset.String(), }, - DestinationAsset: yenAsset, + DestinationAsset: yenAsset.String(), DestinationAmount: 1000, }, { SourceAmount: 10, - SourceAsset: nativeAsset, - sourceAssetString: nativeAsset.String(), - InteriorNodes: []xdr.Asset{}, - DestinationAsset: yenAsset, + SourceAsset: nativeAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: yenAsset.String(), DestinationAmount: 1000, }, } @@ -1143,39 +1218,39 @@ func TestSortAndFilterPathsBySourceAsset(t *testing.T) { expectedPaths := []Path{ { SourceAmount: 2, - SourceAsset: eurAsset, - InteriorNodes: []xdr.Asset{}, - DestinationAsset: yenAsset, + SourceAsset: eurAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: yenAsset.String(), DestinationAmount: 1000, }, { SourceAmount: 2, - SourceAsset: eurAsset, - InteriorNodes: []xdr.Asset{ - nativeAsset, + SourceAsset: eurAsset.String(), + InteriorNodes: []string{ + nativeAsset.String(), }, - DestinationAsset: yenAsset, + DestinationAsset: yenAsset.String(), DestinationAmount: 1000, }, { SourceAmount: 3, - SourceAsset: eurAsset, - InteriorNodes: []xdr.Asset{}, - DestinationAsset: yenAsset, + SourceAsset: eurAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: yenAsset.String(), DestinationAmount: 1000, }, { SourceAmount: 1, - SourceAsset: usdAsset, - InteriorNodes: []xdr.Asset{}, - DestinationAsset: yenAsset, + SourceAsset: usdAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: yenAsset.String(), DestinationAmount: 1000, }, { SourceAmount: 10, - SourceAsset: nativeAsset, - InteriorNodes: []xdr.Asset{}, - DestinationAsset: yenAsset, + SourceAsset: nativeAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: yenAsset.String(), DestinationAmount: 1000, }, } @@ -1186,55 +1261,48 @@ func TestSortAndFilterPathsBySourceAsset(t *testing.T) { func TestSortAndFilterPathsByDestinationAsset(t *testing.T) { allPaths := []Path{ { - SourceAmount: 1000, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{}, - DestinationAsset: eurAsset, - destinationAssetString: eurAsset.String(), - DestinationAmount: 3, + SourceAmount: 1000, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: eurAsset.String(), + DestinationAmount: 3, }, { - SourceAmount: 1000, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{}, - DestinationAsset: eurAsset, - destinationAssetString: eurAsset.String(), - DestinationAmount: 4, + SourceAmount: 1000, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: eurAsset.String(), + DestinationAmount: 4, }, { - SourceAmount: 1000, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{}, - DestinationAsset: usdAsset, - destinationAssetString: usdAsset.String(), - DestinationAmount: 1, + SourceAmount: 1000, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: usdAsset.String(), + DestinationAmount: 1, }, { - SourceAmount: 1000, - SourceAsset: yenAsset, - sourceAssetString: eurAsset.String(), - InteriorNodes: []xdr.Asset{}, - DestinationAsset: eurAsset, - destinationAssetString: eurAsset.String(), - DestinationAmount: 2, + SourceAmount: 1000, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: eurAsset.String(), + DestinationAmount: 2, }, { SourceAmount: 1000, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{ - nativeAsset, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{ + nativeAsset.String(), }, - DestinationAsset: eurAsset, - destinationAssetString: eurAsset.String(), - DestinationAmount: 2, + DestinationAsset: eurAsset.String(), + DestinationAmount: 2, }, { - SourceAmount: 1000, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{}, - DestinationAsset: nativeAsset, - destinationAssetString: nativeAsset.String(), - DestinationAmount: 10, + SourceAmount: 1000, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: nativeAsset.String(), + DestinationAmount: 10, }, } sortedAndFiltered, err := sortAndFilterPaths( @@ -1247,37 +1315,37 @@ func TestSortAndFilterPathsByDestinationAsset(t *testing.T) { expectedPaths := []Path{ { SourceAmount: 1000, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{}, - DestinationAsset: eurAsset, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: eurAsset.String(), DestinationAmount: 4, }, { SourceAmount: 1000, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{}, - DestinationAsset: eurAsset, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: eurAsset.String(), DestinationAmount: 3, }, { SourceAmount: 1000, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{}, - DestinationAsset: eurAsset, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: eurAsset.String(), DestinationAmount: 2, }, { SourceAmount: 1000, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{}, - DestinationAsset: usdAsset, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: usdAsset.String(), DestinationAmount: 1, }, { SourceAmount: 1000, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{}, - DestinationAsset: nativeAsset, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: nativeAsset.String(), DestinationAmount: 10, }, } @@ -1406,29 +1474,29 @@ func TestFindPaths(t *testing.T) { { // arbitrage usd then trade to xlm SourceAmount: 2, - SourceAsset: usdAsset, - InteriorNodes: []xdr.Asset{ - eurAsset, - usdAsset, + SourceAsset: usdAsset.String(), + InteriorNodes: []string{ + eurAsset.String(), + usdAsset.String(), }, - DestinationAsset: nativeAsset, + DestinationAsset: nativeAsset.String(), DestinationAmount: 20, }, { SourceAmount: 5, - SourceAsset: usdAsset, - InteriorNodes: []xdr.Asset{}, - DestinationAsset: nativeAsset, + SourceAsset: usdAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: nativeAsset.String(), DestinationAmount: 20, }, { SourceAmount: 5, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{ - eurAsset, - chfAsset, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{ + eurAsset.String(), + chfAsset.String(), }, - DestinationAsset: nativeAsset, + DestinationAsset: nativeAsset.String(), DestinationAmount: 20, }, } @@ -1482,40 +1550,40 @@ func TestFindPaths(t *testing.T) { { // arbitrage usd then trade to xlm SourceAmount: 2, - SourceAsset: usdAsset, - InteriorNodes: []xdr.Asset{ - eurAsset, - usdAsset, + SourceAsset: usdAsset.String(), + InteriorNodes: []string{ + eurAsset.String(), + usdAsset.String(), }, - DestinationAsset: nativeAsset, + DestinationAsset: nativeAsset.String(), DestinationAmount: 20, }, { SourceAmount: 5, - SourceAsset: usdAsset, - InteriorNodes: []xdr.Asset{}, - DestinationAsset: nativeAsset, + SourceAsset: usdAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: nativeAsset.String(), DestinationAmount: 20, }, { SourceAmount: 2, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{ - usdAsset, - eurAsset, - chfAsset, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{ + usdAsset.String(), + eurAsset.String(), + chfAsset.String(), }, - DestinationAsset: nativeAsset, + DestinationAsset: nativeAsset.String(), DestinationAmount: 20, }, { SourceAmount: 5, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{ - eurAsset, - chfAsset, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{ + eurAsset.String(), + chfAsset.String(), }, - DestinationAsset: nativeAsset, + DestinationAsset: nativeAsset.String(), DestinationAmount: 20, }, } @@ -1547,40 +1615,40 @@ func TestFindPaths(t *testing.T) { { // arbitrage usd then trade to xlm SourceAmount: 2, - SourceAsset: usdAsset, - InteriorNodes: []xdr.Asset{ - eurAsset, - usdAsset, + SourceAsset: usdAsset.String(), + InteriorNodes: []string{ + eurAsset.String(), + usdAsset.String(), }, - DestinationAsset: nativeAsset, + DestinationAsset: nativeAsset.String(), DestinationAmount: 20, }, { SourceAmount: 5, - SourceAsset: usdAsset, - InteriorNodes: []xdr.Asset{}, - DestinationAsset: nativeAsset, + SourceAsset: usdAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: nativeAsset.String(), DestinationAmount: 20, }, { SourceAmount: 2, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{ - usdAsset, - eurAsset, - chfAsset, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{ + usdAsset.String(), + eurAsset.String(), + chfAsset.String(), }, - DestinationAsset: nativeAsset, + DestinationAsset: nativeAsset.String(), DestinationAmount: 20, }, { SourceAmount: 5, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{ - eurAsset, - chfAsset, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{ + eurAsset.String(), + chfAsset.String(), }, - DestinationAsset: nativeAsset, + DestinationAsset: nativeAsset.String(), DestinationAmount: 20, }, } @@ -1686,19 +1754,19 @@ func TestFindPathsStartingAt(t *testing.T) { { // arbitrage usd then trade to xlm SourceAmount: 5, - SourceAsset: usdAsset, - InteriorNodes: []xdr.Asset{ - eurAsset, - usdAsset, + SourceAsset: usdAsset.String(), + InteriorNodes: []string{ + eurAsset.String(), + usdAsset.String(), }, - DestinationAsset: nativeAsset, + DestinationAsset: nativeAsset.String(), DestinationAmount: 60, }, { SourceAmount: 5, - SourceAsset: usdAsset, - InteriorNodes: []xdr.Asset{}, - DestinationAsset: nativeAsset, + SourceAsset: usdAsset.String(), + InteriorNodes: []string{}, + DestinationAsset: nativeAsset.String(), DestinationAmount: 20, }, } @@ -1744,12 +1812,12 @@ func TestFindPathsStartingAt(t *testing.T) { expectedPaths = []Path{ { SourceAmount: 5, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{ - chfAsset, - eurAsset, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{ + chfAsset.String(), + eurAsset.String(), }, - DestinationAsset: nativeAsset, + DestinationAsset: nativeAsset.String(), DestinationAmount: 20, }, } @@ -1775,23 +1843,23 @@ func TestFindPathsStartingAt(t *testing.T) { expectedPaths = []Path{ { SourceAmount: 5, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{ - chfAsset, - eurAsset, - usdAsset, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{ + chfAsset.String(), + eurAsset.String(), + usdAsset.String(), }, - DestinationAsset: nativeAsset, + DestinationAsset: nativeAsset.String(), DestinationAmount: 80, }, { SourceAmount: 5, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{ - chfAsset, - eurAsset, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{ + chfAsset.String(), + eurAsset.String(), }, - DestinationAsset: nativeAsset, + DestinationAsset: nativeAsset.String(), DestinationAmount: 20, }, } @@ -1817,33 +1885,33 @@ func TestFindPathsStartingAt(t *testing.T) { expectedPaths = []Path{ { SourceAmount: 5, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{ - chfAsset, - eurAsset, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{ + chfAsset.String(), + eurAsset.String(), }, - DestinationAsset: usdAsset, + DestinationAsset: usdAsset.String(), DestinationAmount: 20, }, { SourceAmount: 5, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{ - chfAsset, - eurAsset, - usdAsset, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{ + chfAsset.String(), + eurAsset.String(), + usdAsset.String(), }, - DestinationAsset: nativeAsset, + DestinationAsset: nativeAsset.String(), DestinationAmount: 80, }, { SourceAmount: 5, - SourceAsset: yenAsset, - InteriorNodes: []xdr.Asset{ - chfAsset, - eurAsset, + SourceAsset: yenAsset.String(), + InteriorNodes: []string{ + chfAsset.String(), + eurAsset.String(), }, - DestinationAsset: nativeAsset, + DestinationAsset: nativeAsset.String(), DestinationAmount: 20, }, } @@ -1881,11 +1949,11 @@ func TestPathThroughLiquidityPools(t *testing.T) { // exchange 112 Euros. To get 112 EUR, we need to exchange 127 USD. expectedPaths := []Path{ { - SourceAsset: usdAsset, + SourceAsset: usdAsset.String(), SourceAmount: 127, - DestinationAsset: yenAsset, + DestinationAsset: yenAsset.String(), DestinationAmount: 100, - InteriorNodes: []xdr.Asset{eurAsset}, + InteriorNodes: []string{eurAsset.String()}, }, } @@ -1939,11 +2007,11 @@ func TestPathThroughLiquidityPools(t *testing.T) { ) expectedPaths := []Path{{ - SourceAsset: chfAsset, + SourceAsset: chfAsset.String(), SourceAmount: 73, - DestinationAsset: yenAsset, + DestinationAsset: yenAsset.String(), DestinationAmount: 100, - InteriorNodes: []xdr.Asset{usdAsset, eurAsset}, + InteriorNodes: []string{usdAsset.String(), eurAsset.String()}, }} assert.NoError(t, err) @@ -2015,17 +2083,17 @@ func TestInterleavedPaths(t *testing.T) { // If we only go through pools, it's less-so: // 90 CHF for 152 USD for 100 XLM expectedPaths := []Path{{ - SourceAsset: chfAsset, + SourceAsset: chfAsset.String(), SourceAmount: 64, - DestinationAsset: nativeAsset, + DestinationAsset: nativeAsset.String(), DestinationAmount: 100, - InteriorNodes: []xdr.Asset{usdAsset, eurAsset}, + InteriorNodes: []string{usdAsset.String(), eurAsset.String()}, }, { - SourceAsset: chfAsset, + SourceAsset: chfAsset.String(), SourceAmount: 90, - DestinationAsset: nativeAsset, + DestinationAsset: nativeAsset.String(), DestinationAmount: 100, - InteriorNodes: []xdr.Asset{usdAsset}, + InteriorNodes: []string{usdAsset.String()}, }} assert.NoError(t, err) @@ -2040,11 +2108,11 @@ func TestInterleavedPaths(t *testing.T) { ) expectedPaths = []Path{{ - SourceAsset: chfAsset, + SourceAsset: chfAsset.String(), SourceAmount: 96, - DestinationAsset: nativeAsset, + DestinationAsset: nativeAsset.String(), DestinationAmount: 101, - InteriorNodes: []xdr.Asset{usdAsset}, + InteriorNodes: []string{usdAsset.String()}, }} assert.NoError(t, err) @@ -2111,11 +2179,11 @@ func TestInterleavedFixedPaths(t *testing.T) { expectedPaths := []Path{ { - SourceAsset: nativeAsset, + SourceAsset: nativeAsset.String(), SourceAmount: 1234, - DestinationAsset: chfAsset, + DestinationAsset: chfAsset.String(), DestinationAmount: 13, - InteriorNodes: []xdr.Asset{usdAsset}, + InteriorNodes: []string{usdAsset.String()}, }, } @@ -2192,23 +2260,15 @@ func TestRepro(t *testing.T) { // can't, because BTC/YBX pool is too small } -func printPath(path Path) { - fmt.Printf(" - %d %s -> ", path.SourceAmount, getCode(path.SourceAsset)) - - for _, hop := range path.InteriorNodes { - fmt.Printf("%s -> ", getCode(hop)) - } - - fmt.Printf("%d %s\n", - path.DestinationAmount, getCode(path.DestinationAsset)) -} - func makeVenues(offers ...xdr.OfferEntry) Venues { return Venues{offers: offers} } -func makeTradingPair(buying, selling xdr.Asset) tradingPair { - return tradingPair{buyingAsset: buying.String(), sellingAsset: selling.String()} +func makeTradingPair(assetStringToID map[string]int32, buying, selling xdr.Asset) tradingPair { + return tradingPair{ + buyingAsset: assetStringToID[buying.String()], + sellingAsset: assetStringToID[selling.String()], + } } func makePool(A, B xdr.Asset, a, b xdr.Int64) xdr.LiquidityPoolEntry { diff --git a/exp/orderbook/pools.go b/exp/orderbook/pools.go index 9d74469d11..8fda6c4d17 100644 --- a/exp/orderbook/pools.go +++ b/exp/orderbook/pools.go @@ -43,8 +43,8 @@ var ( // Warning: If you pass an asset that is NOT one of the pool reserves, the // behavior of this function is undefined (for performance). func makeTrade( - pool xdr.LiquidityPoolEntry, - asset xdr.Asset, + pool liquidityPool, + asset int32, tradeType int, amount xdr.Int64, ) (xdr.Int64, error) { @@ -59,7 +59,7 @@ func makeTrade( // determine which asset `amount` corresponds to X, Y := details.ReserveA, details.ReserveB - if !details.Params.AssetA.Equals(asset) { + if pool.assetA != asset { X, Y = Y, X } @@ -161,10 +161,9 @@ func calculatePoolExpectation( // getOtherAsset returns the other asset in the liquidity pool. Note that // doesn't check to make sure the passed in `asset` is actually part of the // pool; behavior in that case is undefined. -func getOtherAsset(asset xdr.Asset, pool xdr.LiquidityPoolEntry) xdr.Asset { - cp := pool.Body.MustConstantProduct() - if cp.Params.AssetA.Equals(asset) { - return cp.Params.AssetB +func getOtherAsset(asset int32, pool liquidityPool) int32 { + if pool.assetA == asset { + return pool.assetB } - return cp.Params.AssetA + return pool.assetA } diff --git a/exp/orderbook/pools_test.go b/exp/orderbook/pools_test.go index 82b7d7a162..57aa2ca2bb 100644 --- a/exp/orderbook/pools_test.go +++ b/exp/orderbook/pools_test.go @@ -9,17 +9,32 @@ import ( ) func TestLiquidityPoolExchanges(t *testing.T) { + graph := NewOrderBookGraph() + for _, poolEntry := range []xdr.LiquidityPoolEntry{ + eurUsdLiquidityPool, + eurYenLiquidityPool, + nativeUsdPool, + usdChfLiquidityPool, + } { + params := poolEntry.Body.MustConstantProduct().Params + graph.getOrCreateAssetID(params.AssetA) + graph.getOrCreateAssetID(params.AssetB) + } t.Run("happy path", func(t *testing.T) { - for _, asset := range []xdr.Asset{usdAsset, eurAsset} { - payout, err := makeTrade(eurUsdLiquidityPool, asset, tradeTypeDeposit, 500) + for _, assetXDR := range []xdr.Asset{usdAsset, eurAsset} { + pool := graph.poolFromEntry(eurUsdLiquidityPool) + asset := graph.getOrCreateAssetID(assetXDR) + payout, err := makeTrade(pool, asset, tradeTypeDeposit, 500) assert.NoError(t, err) assert.EqualValues(t, 332, int64(payout)) // reserves would now be: 668 of A, 1500 of B // note pool object is unchanged so looping is safe } - for _, asset := range []xdr.Asset{usdAsset, eurAsset} { - payout, err := makeTrade(eurUsdLiquidityPool, asset, tradeTypeExpectation, 332) + for _, assetXDR := range []xdr.Asset{usdAsset, eurAsset} { + pool := graph.poolFromEntry(eurUsdLiquidityPool) + asset := graph.getOrCreateAssetID(assetXDR) + payout, err := makeTrade(pool, asset, tradeTypeExpectation, 332) assert.NoError(t, err) assert.EqualValues(t, 499, int64(payout)) } @@ -43,13 +58,14 @@ func TestLiquidityPoolExchanges(t *testing.T) { } for _, test := range testTable { - needed, err := makeTrade(test.pool, test.dstAsset, - tradeTypeExpectation, test.expectedPayout) + pool := graph.poolFromEntry(test.pool) + asset := graph.getOrCreateAssetID(test.dstAsset) + needed, err := makeTrade(pool, asset, tradeTypeExpectation, test.expectedPayout) assert.NoError(t, err) assert.EqualValuesf(t, test.expectedInput, needed, "expected exchange of %d %s -> %d %s, got %d", - test.expectedInput, getCode(getOtherAsset(test.dstAsset, test.pool)), + test.expectedInput, graph.idToAssetString[getOtherAsset(asset, pool)], test.expectedPayout, getCode(test.dstAsset), needed) } @@ -58,7 +74,10 @@ func TestLiquidityPoolExchanges(t *testing.T) { t.Run("fail on bad exchange amounts", func(t *testing.T) { badValues := []xdr.Int64{math.MaxInt64, math.MaxInt64 - 99, 0, -100} for _, badValue := range badValues { - _, err := makeTrade(eurUsdLiquidityPool, usdAsset, tradeTypeDeposit, badValue) + pool := graph.poolFromEntry(eurUsdLiquidityPool) + asset := graph.getOrCreateAssetID(usdAsset) + + _, err := makeTrade(pool, asset, tradeTypeDeposit, badValue) assert.Error(t, err) } }) diff --git a/exp/orderbook/search.go b/exp/orderbook/search.go index d65d4ec769..56883f5c0f 100644 --- a/exp/orderbook/search.go +++ b/exp/orderbook/search.go @@ -9,54 +9,41 @@ import ( // Path represents a payment path from a source asset to some destination asset type Path struct { - SourceAsset xdr.Asset + SourceAsset string SourceAmount xdr.Int64 - DestinationAsset xdr.Asset + DestinationAsset string DestinationAmount xdr.Int64 - // sourceAssetString and destinationAssetString are included as an - // optimization to improve the performance of sorting paths by avoiding - // serializing assets to strings repeatedly - sourceAssetString string - destinationAssetString string - - InteriorNodes []xdr.Asset -} - -// SourceAssetString returns the string representation of the path's source asset -func (p *Path) SourceAssetString() string { - if p.sourceAssetString == "" { - p.sourceAssetString = p.SourceAsset.String() - } - return p.sourceAssetString + InteriorNodes []string } -// DestinationAssetString returns the string representation of the path's destination asset -func (p *Path) DestinationAssetString() string { - if p.destinationAssetString == "" { - p.destinationAssetString = p.DestinationAsset.String() - } - return p.destinationAssetString +type liquidityPool struct { + xdr.LiquidityPoolEntry + assetA int32 + assetB int32 } type Venues struct { offers []xdr.OfferEntry - pool xdr.LiquidityPoolEntry // can be empty, check body pointer + pool liquidityPool // can be empty, check body pointer } type searchState interface { + // totalAssets returns the total number of assets in the search space. + totalAssets() int32 + // considerPools returns true if we will consider liquidity pools in our path // finding search. considerPools() bool // isTerminalNode returns true if the current asset is a terminal node in our // path finding search. - isTerminalNode(asset string) bool + isTerminalNode(asset int32) bool // includePath returns true if the current path which ends at the given asset // and produces the given amount satisfies our search criteria. includePath( - currentAsset string, + currentAsset int32, currentAssetAmount xdr.Int64, ) bool @@ -68,8 +55,8 @@ type searchState interface { // appendToPaths appends the current path to our result list. appendToPaths( - path []xdr.Asset, - currentAsset string, + path []int32, + currentAsset int32, currentAssetAmount xdr.Int64, ) @@ -78,7 +65,7 @@ type searchState interface { // The result is grouped by the next asset hop, mapping to a sorted list of // offers (by price) and a liquidity pool (if one exists for that trading // pair). - venues(currentAsset string) edgeSet + venues(currentAsset int32) edgeSet // consumeOffers will consume the given set of offers to trade our // current asset for a different asset. @@ -86,41 +73,40 @@ type searchState interface { currentAssetAmount xdr.Int64, currentBestAmount xdr.Int64, offers []xdr.OfferEntry, - ) (xdr.Asset, xdr.Int64, error) + ) (xdr.Int64, error) // consumePool will consume the given liquidity pool to trade our // current asset for a different asset. consumePool( - pool xdr.LiquidityPoolEntry, - currentAsset xdr.Asset, + pool liquidityPool, + currentAsset int32, currentAssetAmount xdr.Int64, ) (xdr.Int64, error) } type pathNode struct { - assetString string - asset xdr.Asset - prev *pathNode + asset int32 + prev *pathNode } -func (p *pathNode) contains(src, dst string) bool { +func (p *pathNode) contains(src, dst int32) bool { for cur := p; cur != nil && cur.prev != nil; cur = cur.prev { - if cur.assetString == dst && cur.prev.assetString == src { + if cur.asset == dst && cur.prev.asset == src { return true } } return false } -func reversePath(path []xdr.Asset) { +func reversePath(path []int32) { for i := len(path)/2 - 1; i >= 0; i-- { opp := len(path) - 1 - i path[i], path[opp] = path[opp], path[i] } } -func (e *pathNode) path() []xdr.Asset { - var result []xdr.Asset +func (e *pathNode) path() []int32 { + var result []int32 for cur := e; cur != nil; cur = cur.prev { result = append(result, cur.asset) } @@ -133,45 +119,52 @@ func search( ctx context.Context, state searchState, maxPathLength int, - sourceAssetString string, - sourceAsset xdr.Asset, + sourceAsset int32, sourceAssetAmount xdr.Int64, ) error { - bestAmount := map[string]xdr.Int64{sourceAssetString: sourceAssetAmount} - updateAmount := map[string]xdr.Int64{sourceAssetString: sourceAssetAmount} - bestPath := map[string]*pathNode{sourceAssetString: { - assetString: sourceAssetString, - asset: sourceAsset, - prev: nil, - }} + totalAssets := state.totalAssets() + bestAmount := make([]xdr.Int64, totalAssets) + updateAmount := make([]xdr.Int64, totalAssets) + bestPath := make([]*pathNode, totalAssets) + updatePath := make([]*pathNode, totalAssets) + updatedAssets := make([]int32, 0, totalAssets) + bestAmount[sourceAsset] = sourceAssetAmount + updateAmount[sourceAsset] = sourceAssetAmount + bestPath[sourceAsset] = &pathNode{ + asset: sourceAsset, + prev: nil, + } for i := 0; i < maxPathLength; i++ { - updatePath := map[string]*pathNode{} + updatedAssets = updatedAssets[:0] - for currentAssetString, currentAmount := range bestAmount { - pathToCurrentAsset := bestPath[currentAssetString] - currentAsset := pathToCurrentAsset.asset - edges := state.venues(currentAssetString) + for currentAsset := int32(0); currentAsset < totalAssets; currentAsset++ { + currentAmount := bestAmount[currentAsset] + if currentAmount == 0 { + continue + } + pathToCurrentAsset := bestPath[currentAsset] + edges := state.venues(currentAsset) for j := 0; j < len(edges); j++ { // Exit early if the context was cancelled. if err := ctx.Err(); err != nil { return err } - nextAssetString, venues := edges[j].key, edges[j].value + nextAsset, venues := edges[j].key, edges[j].value // If we're on our last step ignore any edges which don't lead to // our desired destination. This optimization will save us from // doing wasted computation. - if i == maxPathLength-1 && !state.isTerminalNode(nextAssetString) { + if i == maxPathLength-1 && !state.isTerminalNode(nextAsset) { continue } // Make sure we don't use an edge more than once. - if pathToCurrentAsset.contains(currentAssetString, nextAssetString) { + if pathToCurrentAsset.contains(currentAsset, nextAsset) { continue } - nextAsset, nextAssetAmount, err := processVenues(state, currentAsset, currentAmount, venues) + nextAssetAmount, err := processVenues(state, currentAsset, currentAmount, venues) if err != nil { return err } @@ -179,26 +172,27 @@ func search( continue } - if bestNextAssetAmount, ok := updateAmount[nextAssetString]; !ok || state.betterPathAmount(bestNextAssetAmount, nextAssetAmount) { - updateAmount[nextAssetString] = nextAssetAmount + if state.betterPathAmount(updateAmount[nextAsset], nextAssetAmount) { + newEntry := updateAmount[nextAsset] == bestAmount[nextAsset] + updateAmount[nextAsset] = nextAssetAmount - if pathToNext, ok := updatePath[nextAssetString]; ok { - pathToNext.prev = pathToCurrentAsset - } else { - updatePath[nextAssetString] = &pathNode{ - assetString: nextAssetString, - asset: nextAsset, - prev: pathToCurrentAsset, + if newEntry { + updatePath[nextAsset] = &pathNode{ + asset: nextAsset, + prev: pathToCurrentAsset, } + updatedAssets = append(updatedAssets, nextAsset) + } else { + updatePath[nextAsset].prev = pathToCurrentAsset } // We could avoid this step until the last iteration, but we would // like to include multiple paths in the response to give the user // other options in case the best path is already consumed. - if state.includePath(nextAssetString, nextAssetAmount) { + if state.includePath(nextAsset, nextAssetAmount) { state.appendToPaths( - append(bestPath[currentAssetString].path(), nextAsset), - nextAssetString, + append(bestPath[currentAsset].path(), nextAsset), + nextAsset, nextAssetAmount, ) } @@ -210,9 +204,9 @@ func search( // the algorithm. This optimization will save us from doing wasted // computation. if i < maxPathLength-1 { - for assetString, betterPath := range updatePath { - bestPath[assetString] = betterPath - bestAmount[assetString] = updateAmount[assetString] + for _, asset := range updatedAssets { + bestPath[asset] = updatePath[asset] + bestAmount[asset] = updateAmount[asset] } } } @@ -231,53 +225,69 @@ func search( // `targetAssets` type sellingGraphSearchState struct { graph *OrderBookGraph - destinationAsset xdr.Asset + destinationAssetString string destinationAssetAmount xdr.Int64 ignoreOffersFrom *xdr.AccountId - targetAssets map[string]xdr.Int64 + targetAssets map[int32]xdr.Int64 validateSourceBalance bool paths []Path includePools bool } -func (state *sellingGraphSearchState) isTerminalNode(currentAsset string) bool { +func (state *sellingGraphSearchState) totalAssets() int32 { + return int32(len(state.graph.idToAssetString)) +} + +func (state *sellingGraphSearchState) isTerminalNode(currentAsset int32) bool { _, ok := state.targetAssets[currentAsset] return ok } -func (state *sellingGraphSearchState) includePath(currentAsset string, currentAssetAmount xdr.Int64) bool { +func (state *sellingGraphSearchState) includePath(currentAsset int32, currentAssetAmount xdr.Int64) bool { targetAssetBalance, ok := state.targetAssets[currentAsset] return ok && (!state.validateSourceBalance || targetAssetBalance >= currentAssetAmount) } func (state *sellingGraphSearchState) betterPathAmount(currentAmount, alternativeAmount xdr.Int64) bool { + if currentAmount == 0 { + return true + } + if alternativeAmount == 0 { + return false + } return alternativeAmount < currentAmount } +func assetIDsToAssetStrings(graph *OrderBookGraph, path []int32) []string { + var result []string + for _, asset := range path { + result = append(result, graph.idToAssetString[asset]) + } + return result +} + func (state *sellingGraphSearchState) appendToPaths( - path []xdr.Asset, - currentAssetString string, + path []int32, + currentAsset int32, currentAssetAmount xdr.Int64, ) { - currentAsset := path[len(path)-1] if len(path) > 2 { path = path[1 : len(path)-1] reversePath(path) } else { - path = []xdr.Asset{} + path = []int32{} } state.paths = append(state.paths, Path{ - sourceAssetString: currentAssetString, SourceAmount: currentAssetAmount, - SourceAsset: currentAsset, - InteriorNodes: path, - DestinationAsset: state.destinationAsset, + SourceAsset: state.graph.idToAssetString[currentAsset], + InteriorNodes: assetIDsToAssetStrings(state.graph, path), + DestinationAsset: state.destinationAssetString, DestinationAmount: state.destinationAssetAmount, }) } -func (state *sellingGraphSearchState) venues(currentAsset string) edgeSet { +func (state *sellingGraphSearchState) venues(currentAsset int32) edgeSet { return state.graph.venuesForSellingAsset[currentAsset] } @@ -285,16 +295,11 @@ func (state *sellingGraphSearchState) consumeOffers( currentAssetAmount xdr.Int64, currentBestAmount xdr.Int64, offers []xdr.OfferEntry, -) (xdr.Asset, xdr.Int64, error) { +) (xdr.Int64, error) { nextAmount, err := consumeOffersForSellingAsset( offers, state.ignoreOffersFrom, currentAssetAmount, currentBestAmount) - var nextAsset xdr.Asset - if len(offers) > 0 { - nextAsset = offers[0].Buying - } - - return nextAsset, positiveMin(currentBestAmount, nextAmount), err + return positiveMin(currentBestAmount, nextAmount), err } func (state *sellingGraphSearchState) considerPools() bool { @@ -302,8 +307,8 @@ func (state *sellingGraphSearchState) considerPools() bool { } func (state *sellingGraphSearchState) consumePool( - pool xdr.LiquidityPoolEntry, - currentAsset xdr.Asset, + pool liquidityPool, + currentAsset int32, currentAssetAmount xdr.Int64, ) (xdr.Int64, error) { // How many of the previous hop do we need to get this amount? @@ -321,18 +326,22 @@ func (state *sellingGraphSearchState) consumePool( // - each payment path must begin with `sourceAsset` type buyingGraphSearchState struct { graph *OrderBookGraph - sourceAsset xdr.Asset + sourceAssetString string sourceAssetAmount xdr.Int64 - targetAssets map[string]bool + targetAssets map[int32]bool paths []Path includePools bool } -func (state *buyingGraphSearchState) isTerminalNode(currentAsset string) bool { +func (state *buyingGraphSearchState) totalAssets() int32 { + return int32(len(state.graph.idToAssetString)) +} + +func (state *buyingGraphSearchState) isTerminalNode(currentAsset int32) bool { return state.targetAssets[currentAsset] } -func (state *buyingGraphSearchState) includePath(currentAsset string, currentAssetAmount xdr.Int64) bool { +func (state *buyingGraphSearchState) includePath(currentAsset int32, currentAssetAmount xdr.Int64) bool { return state.targetAssets[currentAsset] } @@ -341,28 +350,26 @@ func (state *buyingGraphSearchState) betterPathAmount(currentAmount, alternative } func (state *buyingGraphSearchState) appendToPaths( - path []xdr.Asset, - currentAssetString string, + path []int32, + currentAsset int32, currentAssetAmount xdr.Int64, ) { - currentAsset := path[len(path)-1] if len(path) > 2 { path = path[1 : len(path)-1] } else { - path = []xdr.Asset{} + path = []int32{} } state.paths = append(state.paths, Path{ - SourceAmount: state.sourceAssetAmount, - SourceAsset: state.sourceAsset, - InteriorNodes: path, - DestinationAsset: currentAsset, - DestinationAmount: currentAssetAmount, - destinationAssetString: currentAssetString, + SourceAmount: state.sourceAssetAmount, + SourceAsset: state.sourceAssetString, + InteriorNodes: assetIDsToAssetStrings(state.graph, path), + DestinationAsset: state.graph.idToAssetString[currentAsset], + DestinationAmount: currentAssetAmount, }) } -func (state *buyingGraphSearchState) venues(currentAsset string) edgeSet { +func (state *buyingGraphSearchState) venues(currentAsset int32) edgeSet { return state.graph.venuesForBuyingAsset[currentAsset] } @@ -370,15 +377,10 @@ func (state *buyingGraphSearchState) consumeOffers( currentAssetAmount xdr.Int64, currentBestAmount xdr.Int64, offers []xdr.OfferEntry, -) (xdr.Asset, xdr.Int64, error) { +) (xdr.Int64, error) { nextAmount, err := consumeOffersForBuyingAsset(offers, currentAssetAmount) - var nextAsset xdr.Asset - if len(offers) > 0 { - nextAsset = offers[0].Selling - } - - return nextAsset, max(nextAmount, currentBestAmount), err + return max(nextAmount, currentBestAmount), err } func (state *buyingGraphSearchState) considerPools() bool { @@ -386,8 +388,8 @@ func (state *buyingGraphSearchState) considerPools() bool { } func (state *buyingGraphSearchState) consumePool( - pool xdr.LiquidityPoolEntry, - currentAsset xdr.Asset, + pool liquidityPool, + currentAsset int32, currentAssetAmount xdr.Int64, ) (xdr.Int64, error) { return makeTrade(pool, currentAsset, tradeTypeDeposit, currentAssetAmount) @@ -519,14 +521,12 @@ func consumeOffersForBuyingAsset( func processVenues( state searchState, - currentAsset xdr.Asset, + currentAsset int32, currentAssetAmount xdr.Int64, venues Venues, -) (xdr.Asset, xdr.Int64, error) { - var nextAsset xdr.Asset - +) (xdr.Int64, error) { if currentAssetAmount == 0 { - return nextAsset, 0, errAssetAmountIsZero + return 0, errAssetAmountIsZero } // We evaluate the pool venue (if any) before offers, because pool exchange @@ -535,26 +535,23 @@ func processVenues( if pool := venues.pool; state.considerPools() && pool.Body.ConstantProduct != nil { amount, err := state.consumePool(pool, currentAsset, currentAssetAmount) if err == nil { - nextAsset = getOtherAsset(currentAsset, pool) poolAmount = amount } // It's only a true error if the offers fail later, too } if poolAmount == 0 && len(venues.offers) == 0 { - return nextAsset, -1, nil // not really an error + return -1, nil // not really an error } // This will return the pool amount if the LP performs better. - offerAsset, nextAssetAmount, err := state.consumeOffers( + nextAssetAmount, err := state.consumeOffers( currentAssetAmount, poolAmount, venues.offers) // Only error out the offers if the LP trade didn't happen. if err != nil && poolAmount == 0 { - return nextAsset, 0, err - } else if err == nil { - nextAsset = offerAsset + return 0, err } - return nextAsset, nextAssetAmount, nil + return nextAssetAmount, nil } diff --git a/exp/orderbook/utils.go b/exp/orderbook/utils.go index 386ab3eda7..d1fad24a62 100644 --- a/exp/orderbook/utils.go +++ b/exp/orderbook/utils.go @@ -5,9 +5,9 @@ import ( ) // getPoolAssets retrieves string representations of a pool's reserves -func getPoolAssets(pool xdr.LiquidityPoolEntry) (string, string) { +func getPoolAssets(pool xdr.LiquidityPoolEntry) (xdr.Asset, xdr.Asset) { params := pool.Body.MustConstantProduct().Params - return params.AssetA.String(), params.AssetB.String() + return params.AssetA, params.AssetB } func max(a, b xdr.Int64) xdr.Int64 { diff --git a/services/horizon/internal/paths/main.go b/services/horizon/internal/paths/main.go index 4975110362..d5baaf25d8 100644 --- a/services/horizon/internal/paths/main.go +++ b/services/horizon/internal/paths/main.go @@ -20,16 +20,16 @@ type Query struct { // Path is the result returned by a path finder and is tied to the DestinationAmount used in the input query type Path struct { - Path []xdr.Asset - Source xdr.Asset + Path []string + Source string SourceAmount xdr.Int64 - Destination xdr.Asset + Destination string DestinationAmount xdr.Int64 } // Finder finds paths. type Finder interface { - // Return a list of payment paths and the most recent ledger + // Find returns a list of payment paths and the most recent ledger // for a Query of a maximum length `maxLength`. The payment paths // are accurate and consistent with the returned ledger sequence number Find(ctx context.Context, q Query, maxLength uint) ([]Path, uint32, error) diff --git a/services/horizon/internal/resourceadapter/path.go b/services/horizon/internal/resourceadapter/path.go index 7e91fbca7c..b25037d042 100644 --- a/services/horizon/internal/resourceadapter/path.go +++ b/services/horizon/internal/resourceadapter/path.go @@ -2,18 +2,36 @@ package resourceadapter import ( "context" + "fmt" + "strings" "github.com/stellar/go/amount" "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/services/horizon/internal/paths" ) +func extractAsset(asset string, t, c, i *string) error { + if asset == "native" { + *t = asset + return nil + } + parts := strings.Split(asset, "/") + if len(parts) == 3 { + return fmt.Errorf("expected length to be 4 but got %v", parts) + } + *t = parts[0] + *c = parts[1] + *i = parts[2] + return nil +} + // PopulatePath converts the paths.Path into a Path func PopulatePath(ctx context.Context, dest *horizon.Path, p paths.Path) (err error) { dest.DestinationAmount = amount.String(p.DestinationAmount) dest.SourceAmount = amount.String(p.SourceAmount) - err = p.Source.Extract( + err = extractAsset( + p.Source, &dest.SourceAssetType, &dest.SourceAssetCode, &dest.SourceAssetIssuer) @@ -21,7 +39,8 @@ func PopulatePath(ctx context.Context, dest *horizon.Path, p paths.Path) (err er return } - err = p.Destination.Extract( + err = extractAsset( + p.Destination, &dest.DestinationAssetType, &dest.DestinationAssetCode, &dest.DestinationAssetIssuer) @@ -31,7 +50,8 @@ func PopulatePath(ctx context.Context, dest *horizon.Path, p paths.Path) (err er dest.Path = make([]horizon.Asset, len(p.Path)) for i, a := range p.Path { - err = a.Extract( + err = extractAsset( + a, &dest.Path[i].Type, &dest.Path[i].Code, &dest.Path[i].Issuer) From 41b735e7aef4d07605b400e663716db97cc040a5 Mon Sep 17 00:00:00 2001 From: tamirms Date: Mon, 29 Nov 2021 14:04:39 +0000 Subject: [PATCH 2/3] Add comments --- exp/orderbook/graph.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/exp/orderbook/graph.go b/exp/orderbook/graph.go index 8ae6866765..6fa5019cef 100644 --- a/exp/orderbook/graph.go +++ b/exp/orderbook/graph.go @@ -49,9 +49,19 @@ type OBGraph interface { // OrderBookGraph is an in-memory graph representation of all the offers in the // Stellar ledger. type OrderBookGraph struct { - assetStringToID map[string]int32 + // idToAssetString maps an int32 asset id to its string representation. + // Every asset on the OrderBookGraph has an int32 id which indexes into idToAssetString. + // The asset integer ids are largely contiguous. When an asset is completely removed + // from the OrderBookGraph the integer id for that asset will be assigned to the next + // asset which is added to the OrderBookGraph. idToAssetString []string - vacantIDs []int32 + // assetStringToID maps an asset string to its int32 id. + assetStringToID map[string]int32 + // vacantIDs is a list of int32 asset ids which can be mapped to new assets. + // When a new asset is added to the OrderBookGraph we first check if there are + // any available vacantIDs, if so, we will assign the new asset to one of the vacantIDs. + // Otherwise, we will add a new entry to idToAssetString for the new asset. + vacantIDs []int32 // venuesForBuyingAsset maps an asset to all of its buying opportunities, // which may be offers (sorted by price) or a liquidity pools. @@ -200,12 +210,17 @@ func (graph *OrderBookGraph) getOrCreateAssetID(asset xdr.Asset) int32 { if ok { return id } + // before creating a new int32 asset id we will try to use + // a vacant id so that we can plug any empty cells in the + // idToAssetString array. if len(graph.vacantIDs) > 0 { id = graph.vacantIDs[len(graph.vacantIDs)-1] graph.vacantIDs = graph.vacantIDs[:len(graph.vacantIDs)-1] graph.idToAssetString[id] = assetString } else { + // idToAssetString never decreases in length unless we call graph.Clear() id = int32(len(graph.idToAssetString)) + // we assign id to asset graph.idToAssetString = append(graph.idToAssetString, assetString) graph.venuesForBuyingAsset = append(graph.venuesForBuyingAsset, nil) graph.venuesForSellingAsset = append(graph.venuesForSellingAsset, nil) @@ -221,6 +236,10 @@ func (graph *OrderBookGraph) maybeDeleteAsset(asset int32) { if buyingEdgesEmpty && sellingEdgesEmpty { delete(graph.assetStringToID, graph.idToAssetString[asset]) + // When removing an asset we do not resize the idToAssetString array. + // Instead, we allow the cell occupied by the id to be empty. + // The next time we will add an asset to the graph we will allocate the + // id to the new asset. graph.idToAssetString[asset] = "" graph.vacantIDs = append(graph.vacantIDs, asset) } From ad9c53f94aa45b6fcffb8caa0cc3b5265ba3fb09 Mon Sep 17 00:00:00 2001 From: tamirms Date: Tue, 30 Nov 2021 08:42:55 +0000 Subject: [PATCH 3/3] Add tests for path resource adapter --- .../horizon/internal/resourceadapter/path.go | 4 +- .../internal/resourceadapter/path_test.go | 50 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 services/horizon/internal/resourceadapter/path_test.go diff --git a/services/horizon/internal/resourceadapter/path.go b/services/horizon/internal/resourceadapter/path.go index b25037d042..1e7d1ecf36 100644 --- a/services/horizon/internal/resourceadapter/path.go +++ b/services/horizon/internal/resourceadapter/path.go @@ -16,8 +16,8 @@ func extractAsset(asset string, t, c, i *string) error { return nil } parts := strings.Split(asset, "/") - if len(parts) == 3 { - return fmt.Errorf("expected length to be 4 but got %v", parts) + if len(parts) != 3 { + return fmt.Errorf("expected length to be 3 but got %v", parts) } *t = parts[0] *c = parts[1] diff --git a/services/horizon/internal/resourceadapter/path_test.go b/services/horizon/internal/resourceadapter/path_test.go new file mode 100644 index 0000000000..4ade5249ab --- /dev/null +++ b/services/horizon/internal/resourceadapter/path_test.go @@ -0,0 +1,50 @@ +package resourceadapter + +import ( + "context" + "testing" + + "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/services/horizon/internal/paths" + "github.com/stellar/go/xdr" + "github.com/stretchr/testify/assert" +) + +func TestPopulatePath(t *testing.T) { + native := xdr.MustNewNativeAsset() + usdc := xdr.MustNewCreditAsset("USDC", "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML") + bingo := xdr.MustNewCreditAsset("BINGO", "GBZ35ZJRIKJGYH5PBKLKOZ5L6EXCNTO7BKIL7DAVVDFQ2ODJEEHHJXIM") + p := paths.Path{ + Path: []string{bingo.String(), native.String()}, + Source: native.String(), + SourceAmount: 123, + Destination: usdc.String(), + DestinationAmount: 345, + } + + var dest horizon.Path + assert.NoError(t, PopulatePath(context.Background(), &dest, p)) + + assert.Equal(t, horizon.Path{ + SourceAssetType: "native", + SourceAssetCode: "", + SourceAssetIssuer: "", + SourceAmount: "0.0000123", + DestinationAssetType: "credit_alphanum4", + DestinationAssetCode: "USDC", + DestinationAssetIssuer: "GC3C4AKRBQLHOJ45U4XG35ESVWRDECWO5XLDGYADO6DPR3L7KIDVUMML", + DestinationAmount: "0.0000345", + Path: []horizon.Asset{ + { + Type: "credit_alphanum12", + Code: "BINGO", + Issuer: "GBZ35ZJRIKJGYH5PBKLKOZ5L6EXCNTO7BKIL7DAVVDFQ2ODJEEHHJXIM", + }, + { + Type: "native", + Code: "", + Issuer: "", + }, + }, + }, dest) +}