Skip to content

Commit

Permalink
update pow10 functions
Browse files Browse the repository at this point in the history
  • Loading branch information
BrendanChou committed Jun 3, 2024
1 parent e6b6683 commit 2f2ba76
Show file tree
Hide file tree
Showing 16 changed files with 168 additions and 371 deletions.
23 changes: 9 additions & 14 deletions protocol/daemons/pricefeed/client/price_function/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,26 +144,21 @@ func reverseShiftBigFloatSlice(
values []*big.Float,
exponent int32,
) []*big.Float {
unsignedExponent := lib.AbsInt32(exponent)

pow10 := new(big.Float).SetInt(lib.BigPow10(uint64(unsignedExponent)))
p10, inverse := lib.BigPow10(exponent)
p10Float := new(big.Float).SetInt(p10)
updatedValues := make([]*big.Float, 0, len(values))
for _, value := range values {
updatedValues = append(updatedValues, reverseShiftFloatWithPow10(value, pow10, exponent))
newValue := new(big.Float).Set(value)
if inverse {
newValue.Mul(newValue, p10Float)
} else {
newValue.Quo(newValue, p10Float)
}
updatedValues = append(updatedValues, newValue)
}
return updatedValues
}

func reverseShiftFloatWithPow10(value *big.Float, pow10 *big.Float, exponent int32) *big.Float {
if exponent == 0 {
return value
} else if exponent > 0 {
return new(big.Float).Quo(value, pow10)
} else { // exponent < 0
return new(big.Float).Mul(value, pow10)
}
}

// Ticker encodes a ticker response returned by an exchange API. It contains accessors for the ticker's
// ask price, bid price, and last price, which are medianized to compute an exchange price.
type Ticker interface {
Expand Down
48 changes: 0 additions & 48 deletions protocol/daemons/pricefeed/client/price_function/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,54 +261,6 @@ func TestGetUint64MedianFromShiftedBigFloatValues(t *testing.T) {
}
}

func TestReverseShiftBigFloatWithPow10(t *testing.T) {
tests := map[string]struct {
// parameters
floatValue *big.Float
exponent int32

// expectations
expectedUpdatedFloatValue *big.Float
}{
"Success with negative exponent": {
floatValue: new(big.Float).SetPrec(64).SetFloat64(100.123),
exponent: -3,
expectedUpdatedFloatValue: new(big.Float).SetPrec(64).SetFloat64(100_123),
},
"Success with positive exponent": {
floatValue: new(big.Float).SetPrec(64).SetFloat64(100.1),
exponent: 1,
expectedUpdatedFloatValue: new(big.Float).SetPrec(64).SetFloat64(10.01),
},
"Success with exponent of 0": {
floatValue: new(big.Float).SetPrec(64).SetFloat64(100),
exponent: 0,
expectedUpdatedFloatValue: new(big.Float).SetPrec(64).SetFloat64(100),
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
unsignedExponent := lib.AbsInt32(tc.exponent)

pow10 := new(big.Float).SetInt(lib.BigPow10(uint64(unsignedExponent)))

updatedFloatValue := reverseShiftFloatWithPow10(
tc.floatValue,
pow10,
tc.exponent,
)

require.InDeltaSlice(
t,
bigSliceToFloatSlice([]*big.Float{tc.expectedUpdatedFloatValue}),
bigSliceToFloatSlice([]*big.Float{updatedFloatValue}),
deltaPrecision,
)
})
}
}

func TestReverseShiftBigFloatSlice(t *testing.T) {
tests := map[string]struct {
// parameters
Expand Down
59 changes: 30 additions & 29 deletions protocol/lib/big_math.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,43 +27,44 @@ func BigMulPpm(val *big.Int, ppm *big.Int, roundUp bool) *big.Int {
}
}

// BigMulPow10 returns the result of `val * 10^exponent`, in *big.Rat.
func BigMulPow10(
val *big.Int,
exponent int32,
) (
result *big.Rat,
) {
ratPow10 := RatPow10(exponent)
return ratPow10.Mul(
new(big.Rat).SetInt(val),
ratPow10,
)
}

// bigPow10Memo is a cache of the most common exponent value requests. Since bigPow10Memo will be
// accessed from different go-routines, the map should only ever be read from or collision
// could occur.
var bigPow10Memo = warmCache()

// BigPow10 returns the result of `10^exponent`. Caches all calculated values and
// re-uses cached values in any following calls to BigPow10.
func BigPow10(exponent uint64) *big.Int {
result := bigPow10Helper(exponent)
// Copy the result, such that no values can be modified by reference in the
// `bigPow10Memo` cache.
copy := new(big.Int).Set(result)
return copy
// BigPow10 returns the result of `10^abs(exponent)` and whether the exponent is non-negative.
func BigPow10[T int | int32 | int64 | uint | uint32 | uint64](
exponent T,
) (
result *big.Int,
inverse bool,
) {
inverse = exponent < 0
var absExponent uint64
if inverse {
absExponent = uint64(-exponent)
} else {
absExponent = uint64(exponent)
}

return new(big.Int).Set(bigPow10Helper(absExponent)), inverse
}

// RatPow10 returns the result of `10^exponent`. Re-uses the cached values by
// calling bigPow10Helper.
func RatPow10(exponent int32) *big.Rat {
result := new(big.Rat).SetInt(bigPow10Helper(uint64(AbsInt32(exponent))))
if exponent < 0 {
result.Inv(result)
// BigIntMulPow10 returns the result of `input * 10^exponent`, rounding in the direction indicated.
// There is no rounding if `exponent` is non-negative.
func BigIntMulPow10[T int | int32 | int64 | uint | uint32 | uint64](
input *big.Int,
exponent T,
roundUp bool,
) *big.Int {
p10, inverse := BigPow10(exponent)
if inverse {
if roundUp {
return BigDivCeil(input, p10)
}
return new(big.Int).Div(input, p10)
}
return result
return new(big.Int).Mul(p10, input)
}

// BigIntMulPpm takes a `big.Int` and returns the result of `input * ppm / 1_000_000`. This method rounds towards
Expand Down
165 changes: 25 additions & 140 deletions protocol/lib/big_math_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,167 +146,52 @@ func TestBigMulPpm(t *testing.T) {

func TestBigPow10(t *testing.T) {
tests := map[string]struct {
exponent uint64
expectedResult *big.Int
}{
"Regular exponent": {
exponent: 3,
expectedResult: new(big.Int).SetUint64(1000),
},
"Zero exponent": {
exponent: 0,
expectedResult: new(big.Int).SetUint64(1),
},
"One exponent": {
exponent: 1,
expectedResult: new(big.Int).SetUint64(10),
},
"Power of 2": {
exponent: 8,
expectedResult: new(big.Int).SetUint64(100_000_000),
},
"Non-power of 2": {
exponent: 6,
expectedResult: new(big.Int).SetUint64(1_000_000),
},
"Greater than max uint64": {
exponent: 20,
expectedResult: big_testutil.MustFirst(new(big.Int).SetString("100000000000000000000", 10)),
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
result := lib.BigPow10(tc.exponent)
require.Equal(t, tc.expectedResult, result)
})
}
}

func TestBigMulPow10(t *testing.T) {
tests := map[string]struct {
val *big.Int
exponent int32
expectedResult *big.Rat
exponent int64
expectedValue *big.Int
expectedInverse bool
}{
"exponent = 2": {
val: new(big.Int).SetUint64(12345678),
exponent: 2,
expectedResult: big.NewRat(1234567800, 1),
},
"exponent = 10": {
val: new(big.Int).SetUint64(12345678),
exponent: 10,
expectedResult: big.NewRat(123456780000000000, 1),
},
"exponent = 0": {
val: new(big.Int).SetUint64(12345678),
exponent: 0,
expectedResult: big.NewRat(12345678, 1),
},
"exponent = -1": {
val: new(big.Int).SetUint64(12345678),
exponent: -1,
expectedResult: big.NewRat(12345678, 10),
},
"exponent = -3": {
val: new(big.Int).SetUint64(12345678),
exponent: -3,
expectedResult: big.NewRat(12345678, 1000),
},
"exponent = -8": {
val: new(big.Int).SetUint64(12345678),
exponent: -8,
expectedResult: big.NewRat(12345678, 100000000),
},
"0": {0, big.NewInt(1), false},
"1": {1, big.NewInt(10), false},
"2": {2, big.NewInt(100), false},
"3": {3, big.NewInt(1000), false},
"4": {4, big.NewInt(10000), false},
"5": {5, big.NewInt(100000), false},
"20": {20, big_testutil.MustFirst(new(big.Int).SetString("100000000000000000000", 10)), false},
"-1": {-1, big.NewInt(10), true},
"-2": {-2, big.NewInt(100), true},
"-3": {-3, big.NewInt(1000), true},
"-4": {-4, big.NewInt(10000), true},
"-5": {-5, big.NewInt(100000), true},
"-20": {-20, big_testutil.MustFirst(new(big.Int).SetString("100000000000000000000", 10)), true},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
result := lib.BigMulPow10(tc.val, tc.exponent)
require.Equal(t, tc.expectedResult, result)
})
}
}

func TestRatPow10(t *testing.T) {
tests := map[string]struct {
exponent int32
expectedResult *big.Rat
}{
"Positive exponent": {
exponent: 3,
expectedResult: new(big.Rat).SetUint64(1000),
},
"Negative exponent": {
exponent: -3,
expectedResult: new(big.Rat).SetFrac64(1, 1000),
},
"Zero exponent": {
exponent: 0,
expectedResult: new(big.Rat).SetUint64(1),
},
"One exponent": {
exponent: 1,
expectedResult: new(big.Rat).SetUint64(10),
},
"Negative one exponent": {
exponent: -1,
expectedResult: new(big.Rat).SetFrac64(1, 10),
},
"Power of 2": {
exponent: 8,
expectedResult: new(big.Rat).SetUint64(100_000_000),
},
"Negative power of 2": {
exponent: -8,
expectedResult: new(big.Rat).SetFrac64(1, 100_000_000),
},
"Non-power of 2": {
exponent: 6,
expectedResult: new(big.Rat).SetUint64(1_000_000),
},
"Negative non-power of 2": {
exponent: -6,
expectedResult: new(big.Rat).SetFrac64(1, 1_000_000),
},
"Greater than max uint64": {
exponent: 20,
expectedResult: big_testutil.MustFirst(new(big.Rat).SetString("100000000000000000000")),
},
"Denom greater than max uint64": {
exponent: -20,
expectedResult: new(big.Rat).SetFrac(
new(big.Int).SetInt64(1),
big_testutil.MustFirst(
new(big.Int).SetString("100000000000000000000", 10),
),
),
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
result := lib.RatPow10(tc.exponent)
require.Equal(t, tc.expectedResult, result)
value, inverse := lib.BigPow10(tc.exponent)
require.Equal(t, tc.expectedValue, value)
require.Equal(t, tc.expectedInverse, inverse)
})
}
}

func TestBigPow10AllValuesInMemo(t *testing.T) {
exponentString := "1"
for i := 0; i < 100; i++ {
bigValue, ok := new(big.Int).SetString(exponentString, 0)
expected, ok := new(big.Int).SetString(exponentString, 10)

require.True(t, ok)
require.Equal(t, lib.BigPow10(uint64(i)), bigValue)
result, _ := lib.BigPow10(i)
require.Equal(t, expected, result)

exponentString = exponentString + "0"
}
}

func TestBigPow10AllValueNotInMemo(t *testing.T) {
exponentString := "1" + strings.Repeat("0", 110)
bigValue, ok := new(big.Int).SetString(exponentString, 0)
expected, ok := new(big.Int).SetString(exponentString, 10)
require.True(t, ok)
require.Equal(t, lib.BigPow10(uint64(110)), bigValue)
result, _ := lib.BigPow10(110)
require.Equal(t, expected, result)
}

func TestBigIntMulPpm(t *testing.T) {
Expand Down
18 changes: 9 additions & 9 deletions protocol/lib/quantums.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ func BaseToQuoteQuantums(
}

// Otherwise multiply or divide by the 1e^exponent.
ePow := bigPow10Helper(uint64(AbsInt32(exponent)))
if exponent > 0 {
return numResult.Mul(numResult, ePow)
} else {
pow10, inverse := BigPow10(exponent)
if inverse {
// Trucated division (towards zero) instead of Euclidean division.
return numResult.Quo(numResult, ePow)
return numResult.Quo(numResult, pow10)
} else {
return numResult.Mul(numResult, pow10)
}
}

Expand Down Expand Up @@ -75,11 +75,11 @@ func QuoteToBaseQuantums(

// Divide result (towards zero) by 10^(exponent).
exponent := priceExponent + baseCurrencyAtomicResolution - QuoteCurrencyAtomicResolution
power10Exponent := BigPow10(uint64(AbsInt32(exponent)))
if exponent > 0 {
result.Quo(result, power10Exponent)
p10, inverse := BigPow10(exponent)
if inverse {
result.Mul(result, p10)
} else {
result.Mul(result, power10Exponent)
result.Quo(result, p10)
}

// Divide result (towards zero) by priceValue.
Expand Down
Loading

0 comments on commit 2f2ba76

Please sign in to comment.