Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(math): add fuzzer for LegacyDec.ApproxRoot to catch stark deviations #17770

Closed
wants to merge 2 commits into from
Closed
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions math/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package math

import (
"math/big"
"strconv"
"strings"
"testing"
)

Expand All @@ -22,3 +25,114 @@ func FuzzLegacyNewDecFromStr(f *testing.F) {
}
})
}

var (
max5Percent, _ = LegacyNewDecFromStr("5") // 5% max tolerance of difference in values
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
max5Percent, _ = LegacyNewDecFromStr("5") // 5% max tolerance of difference in values
maxTolerance, _ = LegacyNewDecFromStr("5") // 5% max tolerance of difference in values

decDiv100, _ = LegacyNewDecFromStr("0.01")
)

func FuzzLegacyDecApproxRoot(f *testing.F) {
if testing.Short() {
f.Skip("running in -short mode")
}

// 1. Add the corpus: <LEGACY_DEC>,<ROOT>
f.Add("-1000000000.5,5")
f.Add("1000000000.5,5")
f.Add("128,8")
f.Add("-128,8")
f.Add("100000,2")
f.Add("-100000,2")

// 2. Now fuzz it.
f.Fuzz(func(t *testing.T, input string) {
splits := strings.Split(input, ",")
if len(splits) < 2 {
// Invalid input, just skip over it.
return
}
Comment on lines +48 to +53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many input strings are going to be immediate rejected. I suggest tightening the fuzzing function to something like

func(t *testing.T, dec string, nthRoot uint64)

Further, I think you can avoid invalid strings by going through big.Rat:

func(t *testing.T, nomBytes, denomBytes []byte, nthRoot uint64)
   nom := big.Int{}.SetBytes(nomBytes)
   denom := big.Int{}.SetBytes(denomBytes)
  decRat := big.Rat{}.SetFrac(nom, denom)
  dec := big.Float{}.SetRat(decRat)

If you want negative numbers, add a bool argument to the fuzzing function.


decStr, powStr := splits[0], splits[1]
nthRoot, err := strconv.ParseUint(powStr, 10, 64)
if err != nil {
// Invalid input, nothing to do here.
return
}
dec, err := LegacyNewDecFromStr(decStr)
if err != nil {
// Invalid input, nothing to do here.
return
}

// Ensure that we aren't passing in a power larger than the value itself.
nthRootAsDec, err := LegacyNewDecFromStr(powStr)
if err != nil {
// Invalid input, nothing to do here.
return
}
if nthRootAsDec.GTE(dec) {
// nthRoot cannot be greater than or equal to the value itself, return.
return
}

gotApproxSqrt, err := dec.ApproxRoot(nthRoot)
if err != nil {
if strings.Contains(err.Error(), "out of bounds") {
return
}
t.Fatalf("\nGiven: %s, nthRoot: %d\nerr: %v", dec, nthRoot, err)
}

// For more focused usage and easy parity checks, we are just doing only
// square root comparisons, hence any nthRoot != 2 can end the journey here!
if nthRoot != 2 {
return
}

// Firstly ensure that gotApproxSqrt * gotApproxSqrt is
// super duper close to the value of dec.
squared := gotApproxSqrt.Mul(gotApproxSqrt)
if !squared.Equal(dec) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This if condition is just an optimization, right? If so, I'd drop it for clarity.

diff := squared.Sub(dec).Abs().Mul(decDiv100).Quo(dec)
if diff.GTE(max5Percent) {
t.Fatalf("Discrepancy:\n(%s)^2 != %s\n\tGot: %s\nDiscrepancy %%: %s", gotApproxSqrt, dec, squared, diff)
}
}

// By this point we are dealing with square root.
// Now roundtrip to ensure that the difference between the
// expected value and that approximation isn't off by 5%.
stdlibFloat, ok := new(big.Float).SetString(decStr)
if !ok {
return
}
origWasNegative := stdlibFloat.Sign() == -1
if origWasNegative {
// Make it an absolute value to avoid panics
// due to passing in negative values into .Sqrt.
stdlibFloat = new(big.Float).Abs(stdlibFloat)
}

stdlibSqrt := new(big.Float).Sqrt(stdlibFloat)
if origWasNegative {
// Invert the sign to maintain parity with cosmossdk.io/math.LegacyDec.ApproxRoot
// which returns a negative value even for square roots.
stdlibSqrt = new(big.Float).Neg(stdlibSqrt)
}

stdlibSqrtAsDec, err := LegacyNewDecFromStr(stdlibSqrt.String())
if err != nil {
return
}

diff := stdlibSqrtAsDec.Sub(gotApproxSqrt).Abs().Mul(decDiv100).Quo(gotApproxSqrt)
if diff.IsNegative() {
diff = diff.Neg()
}
if diff.GT(max5Percent) {
t.Fatalf("\nGiven: sqrt(%s)\nPrecision loss as the difference %s > %s\n"+
"Stdlib sqrt: %+60s\ncosmossdk.io/math.*Dec.ApproxSqrt: %+60s",
dec, diff, max5Percent, stdlibSqrtAsDec, gotApproxSqrt)
}
})
}