Skip to content

Commit

Permalink
exp/orderbook: Use uint256 for pool calculations to improve performan…
Browse files Browse the repository at this point in the history
…ce (#4113)

* Use uint256 for pool calculations to improve performance

Co-authored-by: tamirms <[email protected]>
  • Loading branch information
2opremio and tamirms authored Dec 2, 2021
1 parent db1a59e commit 1a8c282
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 20 deletions.
42 changes: 22 additions & 20 deletions exp/orderbook/pools.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package orderbook

import (
"math"
"math/big"

"github.com/holiman/uint256"
"github.com/stellar/go/support/errors"
"github.com/stellar/go/xdr"
)
Expand Down Expand Up @@ -91,21 +91,21 @@ func makeTrade(
//
// It returns false if the calculation overflows.
func calculatePoolPayout(reserveA, reserveB, received xdr.Int64, feeBips xdr.Int32) (xdr.Int64, bool) {
X, Y := big.NewInt(int64(reserveA)), big.NewInt(int64(reserveB))
F, x := big.NewInt(int64(feeBips)), big.NewInt(int64(received))
X, Y := uint256.NewInt(uint64(reserveA)), uint256.NewInt(uint64(reserveB))
F, x := uint256.NewInt(uint64(feeBips)), uint256.NewInt(uint64(received))

// would this deposit overflow the reserve?
if received > math.MaxInt64-reserveA {
return 0, false
}

// We do all of the math in bips, so it's all upscaled by this value.
maxBips := big.NewInt(10000)
f := new(big.Int).Sub(maxBips, F) // upscaled 1 - F
maxBips := uint256.NewInt(10000)
f := new(uint256.Int).Sub(maxBips, F) // upscaled 1 - F

// right half: X + (1 - F)x
denom := X.Mul(X, maxBips).Add(X, new(big.Int).Mul(x, f))
if denom.Cmp(big.NewInt(0)) == 0 { // avoid div-by-zero panic
denom := X.Mul(X, maxBips).Add(X, new(uint256.Int).Mul(x, f))
if denom.IsZero() { // avoid div-by-zero panic
return 0, false
}

Expand All @@ -115,8 +115,8 @@ func calculatePoolPayout(reserveA, reserveB, received xdr.Int64, feeBips xdr.Int
// divide & check overflow
result := numer.Div(numer, denom)

i := xdr.Int64(result.Int64())
return i, result.IsInt64() && i > 0
val := xdr.Int64(result.Uint64())
return val, result.IsUint64() && val >= 0
}

// calculatePoolExpectation determines how much of `reserveA` you would need to
Expand All @@ -128,34 +128,36 @@ func calculatePoolPayout(reserveA, reserveB, received xdr.Int64, feeBips xdr.Int
func calculatePoolExpectation(
reserveA, reserveB, disbursed xdr.Int64, feeBips xdr.Int32,
) (xdr.Int64, bool) {
X, Y := big.NewInt(int64(reserveA)), big.NewInt(int64(reserveB))
F, y := big.NewInt(int64(feeBips)), big.NewInt(int64(disbursed))
X, Y := uint256.NewInt(uint64(reserveA)), uint256.NewInt(uint64(reserveB))
F, y := uint256.NewInt(uint64(feeBips)), uint256.NewInt(uint64(disbursed))

// sanity check: disbursing shouldn't underflow the reserve
if disbursed >= reserveB {
return 0, false
}

// We do all of the math in bips, so it's all upscaled by this value.
maxBips := big.NewInt(10000)
f := new(big.Int).Sub(maxBips, F) // upscaled 1 - F
maxBips := uint256.NewInt(10000)
f := new(uint256.Int).Sub(maxBips, F) // upscaled 1 - F

denom := Y.Sub(Y, y).Mul(Y, f) // right half: (Y - y)(1 - F)
if denom.Cmp(big.NewInt(0)) == 0 { // avoid div-by-zero panic
denom := Y.Sub(Y, y).Mul(Y, f) // right half: (Y - y)(1 - F)
if denom.IsZero() { // avoid div-by-zero panic
return 0, false
}

numer := X.Mul(X, y).Mul(X, maxBips) // left half: Xy

result, rem := new(big.Int), new(big.Int)
result.DivMod(numer, denom, rem)
result, rem := new(uint256.Int), new(uint256.Int)
result.Div(numer, denom)
rem.Mod(numer, denom)

// hacky way to ceil(): if there's a remainder, add 1
if rem.Cmp(big.NewInt(0)) > 0 {
result.Add(result, big.NewInt(1))
if !rem.IsZero() {
result.AddUint64(result, 1)
}

return xdr.Int64(result.Int64()), result.IsInt64()
val := xdr.Int64(result.Uint64())
return val, result.IsUint64() && val >= 0
}

// getOtherAsset returns the other asset in the liquidity pool. Note that
Expand Down
123 changes: 123 additions & 0 deletions exp/orderbook/pools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package orderbook

import (
"math"
"math/big"
"math/rand"
"testing"

"github.com/stellar/go/xdr"
Expand Down Expand Up @@ -166,6 +168,25 @@ func TestLiquidityPoolMath(t *testing.T) {
// ceil(10000 / 0.997) = 10031 we need to receive 10000.
assertPoolExchange(t, recv, 10000, -1, 20000, 10000, 30, true, 10031, -1)
})

t.Run("Potential Internal Overflow", func(t *testing.T) {

// Test for internal uint128 underflow/overflow in calculatePoolPayout() and calculatePoolExpectation() by providing
// input values which cause the maximum internal calculations

assertPoolExchange(t, send, math.MaxInt64, math.MaxInt64, math.MaxInt64, math.MaxInt64, 0, false, 0, 0)
assertPoolExchange(t, send, math.MaxInt64, math.MaxInt64, math.MaxInt64, math.MaxInt64, 0, false, 0, 0)
assertPoolExchange(t, recv, math.MaxInt64, math.MaxInt64, math.MaxInt64, 0, 0, false, 0, 0)

// Check with reserveB < disbursed
assertPoolExchange(t, recv, math.MaxInt64, math.MaxInt64, 0, 1, 0, false, 0, 0)

// Check with poolFeeBips > 10000
assertPoolExchange(t, send, math.MaxInt64, math.MaxInt64, math.MaxInt64, math.MaxInt64, 10001, false, 0, 0)
assertPoolExchange(t, recv, math.MaxInt64, math.MaxInt64, math.MaxInt64, 0, 10010, false, 0, 0)

assertPoolExchange(t, send, 92017260901926686, 9157376027422527, 4000000000000000000, 30, 1, false, 0, 0)
})
}

// assertPoolExchange validates that pool inputs match their expected outputs.
Expand Down Expand Up @@ -199,3 +220,105 @@ func assertPoolExchange(t *testing.T,
assert.EqualValues(t, expectedDeposited, toPool, "wrong expectation")
}
}

func TestCalculatePoolExpectations(t *testing.T) {
for i := 0; i < 1000000; i++ {
reserveA := xdr.Int64(rand.Int63())
reserveB := xdr.Int64(rand.Int63())
disbursed := xdr.Int64(rand.Int63())

result, ok := calculatePoolExpectationBig(reserveA, reserveB, disbursed, 30)
result1, ok1 := calculatePoolExpectation(reserveA, reserveB, disbursed, 30)
if assert.Equal(t, ok, ok1) {
assert.Equal(t, result, result1)
}
}
}

func TestCalculatePoolPayout(t *testing.T) {
for i := 0; i < 1000000; i++ {
reserveA := xdr.Int64(rand.Int63())
reserveB := xdr.Int64(rand.Int63())
received := xdr.Int64(rand.Int63())

result, ok := calculatePoolPayoutBig(reserveA, reserveB, received, 30)
result1, ok1 := calculatePoolPayout(reserveA, reserveB, received, 30)
if assert.Equal(t, ok, ok1) {
assert.Equal(t, result, result1)
}
}
}

// calculatePoolPayout calculates the amount of `reserveB` disbursed from the
// pool for a `received` amount of `reserveA` . From CAP-38:
//
// y = floor[(1 - F) Yx / (X + x - Fx)]
//
// It returns false if the calculation overflows.
func calculatePoolPayoutBig(reserveA, reserveB, received xdr.Int64, feeBips xdr.Int32) (xdr.Int64, bool) {
X, Y := big.NewInt(int64(reserveA)), big.NewInt(int64(reserveB))
F, x := big.NewInt(int64(feeBips)), big.NewInt(int64(received))

// would this deposit overflow the reserve?
if received > math.MaxInt64-reserveA {
return 0, false
}

// We do all of the math in bips, so it's all upscaled by this value.
maxBips := big.NewInt(10000)
f := new(big.Int).Sub(maxBips, F) // upscaled 1 - F

// right half: X + (1 - F)x
denom := X.Mul(X, maxBips).Add(X, new(big.Int).Mul(x, f))
if denom.Cmp(big.NewInt(0)) == 0 { // avoid div-by-zero panic
return 0, false
}

// left half, a: (1 - F) Yx
numer := Y.Mul(Y, x).Mul(Y, f)

// divide & check overflow
result := numer.Div(numer, denom)

i := xdr.Int64(result.Int64())
return i, result.IsInt64() && i >= 0
}

// calculatePoolExpectation determines how much of `reserveA` you would need to
// put into a pool to get the `disbursed` amount of `reserveB`.
//
// x = ceil[Xy / ((Y - y)(1 - F))]
//
// It returns false if the calculation overflows.
func calculatePoolExpectationBig(
reserveA, reserveB, disbursed xdr.Int64, feeBips xdr.Int32,
) (xdr.Int64, bool) {
X, Y := big.NewInt(int64(reserveA)), big.NewInt(int64(reserveB))
F, y := big.NewInt(int64(feeBips)), big.NewInt(int64(disbursed))

// sanity check: disbursing shouldn't underflow the reserve
if disbursed >= reserveB {
return 0, false
}

// We do all of the math in bips, so it's all upscaled by this value.
maxBips := big.NewInt(10000)
f := new(big.Int).Sub(maxBips, F) // upscaled 1 - F

denom := Y.Sub(Y, y).Mul(Y, f) // right half: (Y - y)(1 - F)
if denom.Cmp(big.NewInt(0)) == 0 { // avoid div-by-zero panic
return 0, false
}

numer := X.Mul(X, y).Mul(X, maxBips) // left half: Xy

result, rem := new(big.Int), new(big.Int)
result.DivMod(numer, denom, rem)

// hacky way to ceil(): if there's a remainder, add 1
if rem.Cmp(big.NewInt(0)) > 0 {
result.Add(result, big.NewInt(1))
}

return xdr.Int64(result.Int64()), result.IsInt64()
}
1 change: 1 addition & 0 deletions go.list
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ github.com/gorilla/schema v1.1.0
github.com/graph-gophers/graphql-go v0.0.0-20190225005345-3e8838d4614c
github.com/guregu/null v2.1.3-0.20151024101046-79c5bd36b615+incompatible
github.com/hashicorp/golang-lru v0.5.1
github.com/holiman/uint256 v1.2.0
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c
github.com/hpcloud/tail v1.0.0
github.com/imkira/go-interpol v1.1.0
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ require (
github.com/gorilla/schema v1.1.0
github.com/graph-gophers/graphql-go v0.0.0-20190225005345-3e8838d4614c
github.com/guregu/null v2.1.3-0.20151024101046-79c5bd36b615+incompatible
github.com/holiman/uint256 v1.2.0
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c
github.com/imkira/go-interpol v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ github.com/guregu/null v2.1.3-0.20151024101046-79c5bd36b615+incompatible/go.mod
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM=
github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw=
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0=
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
Expand Down

0 comments on commit 1a8c282

Please sign in to comment.