-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,9 @@ | ||
package math | ||
|
||
import ( | ||
"math/big" | ||
"strconv" | ||
"strings" | ||
"testing" | ||
) | ||
|
||
|
@@ -22,3 +25,114 @@ func FuzzLegacyNewDecFromStr(f *testing.F) { | |
} | ||
}) | ||
} | ||
|
||
var ( | ||
max5Percent, _ = 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 |
||
|
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
} | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.