From 1a8c2827bfad7f62b8c21fd900c40d63a52fd369 Mon Sep 17 00:00:00 2001 From: Alfonso Acosta Date: Thu, 2 Dec 2021 16:09:58 +0100 Subject: [PATCH] exp/orderbook: Use uint256 for pool calculations to improve performance (#4113) * Use uint256 for pool calculations to improve performance Co-authored-by: tamirms --- exp/orderbook/pools.go | 42 ++++++------ exp/orderbook/pools_test.go | 123 ++++++++++++++++++++++++++++++++++++ go.list | 1 + go.mod | 1 + go.sum | 2 + 5 files changed, 149 insertions(+), 20 deletions(-) diff --git a/exp/orderbook/pools.go b/exp/orderbook/pools.go index 8fda6c4d17..766401771b 100644 --- a/exp/orderbook/pools.go +++ b/exp/orderbook/pools.go @@ -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" ) @@ -91,8 +91,8 @@ 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 { @@ -100,12 +100,12 @@ func calculatePoolPayout(reserveA, reserveB, received xdr.Int64, feeBips xdr.Int } // 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 } @@ -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 @@ -128,8 +128,8 @@ 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 { @@ -137,25 +137,27 @@ func calculatePoolExpectation( } // 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 diff --git a/exp/orderbook/pools_test.go b/exp/orderbook/pools_test.go index 57aa2ca2bb..67d56749d9 100644 --- a/exp/orderbook/pools_test.go +++ b/exp/orderbook/pools_test.go @@ -2,6 +2,8 @@ package orderbook import ( "math" + "math/big" + "math/rand" "testing" "github.com/stellar/go/xdr" @@ -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. @@ -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() +} diff --git a/go.list b/go.list index d4e4ec7836..3fb92dc865 100644 --- a/go.list +++ b/go.list @@ -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 diff --git a/go.mod b/go.mod index 243d50b378..efc80bfcc4 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 94780709b6..39c4fedc7d 100644 --- a/go.sum +++ b/go.sum @@ -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=