diff --git a/math/CHANGELOG.md b/math/CHANGELOG.md index d05484340906..7ccd0e252390 100644 --- a/math/CHANGELOG.md +++ b/math/CHANGELOG.md @@ -36,8 +36,17 @@ Ref: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.j ## [Unreleased] +## [math/v1.4.0](https://github.com/cosmos/cosmos-sdk/releases/tag/math/v1.4.0) - 2024-01-20 + +### Features + * [#20034](https://github.com/cosmos/cosmos-sdk/pull/20034) Significantly speedup LegacyDec.QuoTruncate and LegacyDec.QuoRoundUp. +### Bug fixes + +* Fix [ASA-2024-010: Math](https://github.com/cosmos/cosmos-sdk/security/advisories/GHSA-7225-m954-23v7) Bit length differences between Int and Dec + + ## [math/v1.3.0](https://github.com/cosmos/cosmos-sdk/releases/tag/math/v1.3.0) - 2024-02-22 ### Features diff --git a/math/dec.go b/math/dec.go index 76a37113b6b8..16bb0806861b 100644 --- a/math/dec.go +++ b/math/dec.go @@ -22,22 +22,20 @@ const ( // LegacyDecimalPrecisionBits bits required to represent the above precision // Ceiling[Log2[10^Precision - 1]] + // Deprecated: This is unused and will be removed LegacyDecimalPrecisionBits = 60 - // decimalTruncateBits is the minimum number of bits removed - // by a truncate operation. It is equal to - // Floor[Log2[10^Precision - 1]]. - decimalTruncateBits = LegacyDecimalPrecisionBits - 1 - - maxDecBitLen = MaxBitLen + decimalTruncateBits - // maxApproxRootIterations max number of iterations in ApproxRoot function maxApproxRootIterations = 300 ) var ( - precisionReuse = new(big.Int).Exp(big.NewInt(10), big.NewInt(LegacyPrecision), nil) - fivePrecision = new(big.Int).Quo(precisionReuse, big.NewInt(2)) + precisionReuse = new(big.Int).Exp(big.NewInt(10), big.NewInt(LegacyPrecision), nil) + fivePrecision = new(big.Int).Quo(precisionReuse, big.NewInt(2)) + + upperLimit LegacyDec + lowerLimit LegacyDec + precisionMultipliers []*big.Int zeroInt = big.NewInt(0) oneInt = big.NewInt(1) @@ -58,6 +56,11 @@ func init() { for i := 0; i <= LegacyPrecision; i++ { precisionMultipliers[i] = calcPrecisionMultiplier(int64(i)) } + // 2^256 * 10^18 -1 + tmp := new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil) + tmp = new(big.Int).Sub(new(big.Int).Mul(tmp, precisionReuse), big.NewInt(1)) + upperLimit = LegacyNewDecFromBigIntWithPrec(tmp, LegacyPrecision) + lowerLimit = upperLimit.Neg() } func precisionInt() *big.Int { @@ -191,14 +194,15 @@ func LegacyNewDecFromStr(str string) (LegacyDec, error) { if !ok { return LegacyDec{}, fmt.Errorf("failed to set decimal string with base 10: %s", combinedStr) } - if combined.BitLen() > maxDecBitLen { - return LegacyDec{}, fmt.Errorf("decimal '%s' out of range; bitLen: got %d, max %d", str, combined.BitLen(), maxDecBitLen) - } if neg { combined = new(big.Int).Neg(combined) } - return LegacyDec{combined}, nil + result := LegacyDec{i: combined} + if !result.IsInValidRange() { + return LegacyDec{}, fmt.Errorf("out of range: %w", ErrLegacyInvalidDecimalStr) + } + return result, nil } // LegacyMustNewDecFromStr Decimal from string, panic on error @@ -275,9 +279,7 @@ func (d LegacyDec) Add(d2 LegacyDec) LegacyDec { func (d LegacyDec) AddMut(d2 LegacyDec) LegacyDec { d.i.Add(d.i, d2.i) - if d.i.BitLen() > maxDecBitLen { - panic("Int overflow") - } + d.assertInValidRange() return d } @@ -290,10 +292,20 @@ func (d LegacyDec) Sub(d2 LegacyDec) LegacyDec { func (d LegacyDec) SubMut(d2 LegacyDec) LegacyDec { d.i.Sub(d.i, d2.i) - if d.i.BitLen() > maxDecBitLen { + d.assertInValidRange() + return d +} + +func (d LegacyDec) assertInValidRange() { + if !d.IsInValidRange() { panic("Int overflow") } - return d +} + +// IsInValidRange returns true when the value is between the upper limit of (2^256 * 10^18) +// and the lower limit of -1*(2^256 * 10^18). +func (d LegacyDec) IsInValidRange() bool { + return !(d.GT(upperLimit) || d.LT(lowerLimit)) } // Mul multiplication @@ -306,10 +318,8 @@ func (d LegacyDec) MulMut(d2 LegacyDec) LegacyDec { d.i.Mul(d.i, d2.i) chopped := chopPrecisionAndRound(d.i) - if chopped.BitLen() > maxDecBitLen { - panic("Int overflow") - } *d.i = *chopped + d.assertInValidRange() return d } @@ -322,10 +332,7 @@ func (d LegacyDec) MulTruncate(d2 LegacyDec) LegacyDec { func (d LegacyDec) MulTruncateMut(d2 LegacyDec) LegacyDec { d.i.Mul(d.i, d2.i) chopPrecisionAndTruncate(d.i) - - if d.i.BitLen() > maxDecBitLen { - panic("Int overflow") - } + d.assertInValidRange() return d } @@ -339,9 +346,7 @@ func (d LegacyDec) MulRoundUpMut(d2 LegacyDec) LegacyDec { d.i.Mul(d.i, d2.i) chopPrecisionAndRoundUp(d.i) - if d.i.BitLen() > maxDecBitLen { - panic("Int overflow") - } + d.assertInValidRange() return d } @@ -352,9 +357,7 @@ func (d LegacyDec) MulInt(i Int) LegacyDec { func (d LegacyDec) MulIntMut(i Int) LegacyDec { d.i.Mul(d.i, i.BigIntMut()) - if d.i.BitLen() > maxDecBitLen { - panic("Int overflow") - } + d.assertInValidRange() return d } @@ -365,10 +368,7 @@ func (d LegacyDec) MulInt64(i int64) LegacyDec { func (d LegacyDec) MulInt64Mut(i int64) LegacyDec { d.i.Mul(d.i, big.NewInt(i)) - - if d.i.BitLen() > maxDecBitLen { - panic("Int overflow") - } + d.assertInValidRange() return d } @@ -386,9 +386,7 @@ func (d LegacyDec) QuoMut(d2 LegacyDec) LegacyDec { d.i.Quo(d.i, d2.i) chopPrecisionAndRound(d.i) - if d.i.BitLen() > maxDecBitLen { - panic("Int overflow") - } + d.assertInValidRange() return d } @@ -403,9 +401,7 @@ func (d LegacyDec) QuoTruncateMut(d2 LegacyDec) LegacyDec { d.i.Mul(d.i, precisionReuse) d.i.Quo(d.i, d2.i) - if d.i.BitLen() > maxDecBitLen { - panic("Int overflow") - } + d.assertInValidRange() return d } @@ -423,10 +419,7 @@ func (d LegacyDec) QuoRoundupMut(d2 LegacyDec) LegacyDec { rem.Sign() < 0 && d.IsNegative() != d2.IsNegative() { d.i.Add(d.i, oneInt) } - - if d.i.BitLen() > maxDecBitLen { - panic("Int overflow") - } + d.assertInValidRange() return d } @@ -745,17 +738,17 @@ func (d LegacyDec) Ceil() LegacyDec { quo, rem = quo.QuoRem(tmp, precisionReuse, rem) // no need to round with a zero remainder regardless of sign - if rem.Sign() == 0 { - return LegacyNewDecFromBigInt(quo) - } else if rem.Sign() == -1 { - return LegacyNewDecFromBigInt(quo) - } - - if d.i.BitLen() >= maxDecBitLen { - panic("Int overflow") + var r LegacyDec + switch rem.Sign() { + case 0: + r = LegacyNewDecFromBigInt(quo) + case -1: + r = LegacyNewDecFromBigInt(quo) + default: + r = LegacyNewDecFromBigInt(quo.Add(quo, oneInt)) } - - return LegacyNewDecFromBigInt(quo.Add(quo, oneInt)) + r.assertInValidRange() + return r } // LegacyMaxSortableDec is the largest Dec that can be passed into SortableDecBytes() @@ -885,10 +878,9 @@ func (d *LegacyDec) Unmarshal(data []byte) error { return err } - if d.i.BitLen() > maxDecBitLen { - return fmt.Errorf("decimal out of range; got: %d, max: %d", d.i.BitLen(), maxDecBitLen) + if !d.IsInValidRange() { + return errors.New("decimal out of range") } - return nil } diff --git a/math/dec_test.go b/math/dec_test.go index 316b6aaa63dc..4f3897a0867e 100644 --- a/math/dec_test.go +++ b/math/dec_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "sigs.k8s.io/yaml" @@ -96,6 +97,7 @@ func (s *decimalTestSuite) TestNewDecFromStr() { {"8888888888888888888888888888888888888888888888888888888888888888888844444440", false, math.LegacyNewDecFromBigInt(largerBigInt)}, {"33499189745056880149688856635597007162669032647290798121690100488888732861290.034376435130433535", false, math.LegacyNewDecFromBigIntWithPrec(largestBigInt, 18)}, {"133499189745056880149688856635597007162669032647290798121690100488888732861291", true, math.LegacyDec{}}, + {"115792089237316195423570985008687907853269984665640564039457584007913129639936", true, math.LegacyDec{}}, // 2^256 } for tcIndex, tc := range tests { @@ -409,9 +411,10 @@ func (s *decimalTestSuite) TestDecCeil() { } func (s *decimalTestSuite) TestCeilOverflow() { - d, err := math.LegacyNewDecFromStr("66749594872528440074844428317798503581334516323645399060845050244444366430645.000000000000000001") + // (2^256 * 10^18 -1) / 10^18 + d, err := math.LegacyNewDecFromStr("115792089237316195423570985008687907853269984665640564039457584007913129639935.999999999999999999") s.Require().NoError(err) - s.Require().True(d.BigInt().BitLen() <= 315, "d is too large") + s.Require().True(d.IsInValidRange()) // this call panics because the value is too large s.Require().Panics(func() { d.Ceil() }, "Ceil should panic on overflow") } @@ -450,8 +453,8 @@ func (s *decimalTestSuite) TestApproxRoot() { expected math.LegacyDec }{ {math.LegacyNewDecFromInt(math.NewInt(2)), 0, math.LegacyOneDec()}, // 2 ^ 0 => 1.0 - {math.LegacyNewDecWithPrec(4, 2), 0, math.LegacyOneDec()}, // 0.04 ^ 0 => 1.0 - {math.LegacyNewDec(0), 1, math.LegacyNewDec(0)}, // 0 ^ 1 => 0 + {math.LegacyNewDecWithPrec(4, 2), 0, math.LegacyOneDec()}, // 0.04 ^ 0 => 1.0 + {math.LegacyNewDec(0), 1, math.LegacyNewDec(0)}, // 0 ^ 1 => 0 {math.LegacyOneDec(), 10, math.LegacyOneDec()}, // 1.0 ^ (0.1) => 1.0 {math.LegacyNewDecWithPrec(25, 2), 2, math.LegacyNewDecWithPrec(5, 1)}, // 0.25 ^ (0.5) => 0.5 {math.LegacyNewDecWithPrec(4, 2), 2, math.LegacyNewDecWithPrec(2, 1)}, // 0.04 ^ (0.5) => 0.2 @@ -1068,3 +1071,259 @@ func Test_DocumentLegacyAsymmetry(t *testing.T) { require.NotEqual(t, emptyDecJSON, emptyDecRoundTripJSON) require.NotEqual(t, emptyDec, emptyDecRoundTrip) } + +// 2^256 * 10^18 -1 +const maxValidDecNumber = "115792089237316195423570985008687907853269984665640564039457584007913129639935999999999999999999" + +func TestDecOpsWithinLimits(t *testing.T) { + maxValid, ok := new(big.Int).SetString(maxValidDecNumber, 10) + require.True(t, ok) + minValid := new(big.Int).Neg(maxValid) + specs := map[string]struct { + src *big.Int + expErr bool + }{ + "max": { + src: maxValid, + }, + "max + 1": { + src: new(big.Int).Add(maxValid, big.NewInt(1)), + expErr: true, + }, + "min": { + src: minValid, + }, + "min - 1": { + src: new(big.Int).Sub(minValid, big.NewInt(1)), + expErr: true, + }, + "max Int": { + // max Int is 2^256 -1 + src: math.NewIntFromBigInt(new(big.Int).Sub(new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), big.NewInt(1))).BigIntMut(), + }, + "min Int": { + // max Int is -1 *(2^256 -1) + src: math.NewIntFromBigInt(new(big.Int).Neg(new(big.Int).Sub(new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil), big.NewInt(1)))).BigIntMut(), + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + src := math.LegacyNewDecFromBigIntWithPrec(spec.src, 18) + + ops := map[string]struct { + fn func(src math.LegacyDec) math.LegacyDec + }{ + "AddMut": { + fn: func(src math.LegacyDec) math.LegacyDec { return src.AddMut(math.LegacyNewDec(0)) }, + }, + "SubMut": { + fn: func(src math.LegacyDec) math.LegacyDec { return src.SubMut(math.LegacyNewDec(0)) }, + }, + "MulMut": { + fn: func(src math.LegacyDec) math.LegacyDec { return src.MulMut(math.LegacyNewDec(1)) }, + }, + "MulTruncateMut": { + fn: func(src math.LegacyDec) math.LegacyDec { return src.MulTruncateMut(math.LegacyNewDec(1)) }, + }, + "MulRoundUpMut": { + fn: func(src math.LegacyDec) math.LegacyDec { return src.MulRoundUpMut(math.LegacyNewDec(1)) }, + }, + "MulIntMut": { + fn: func(src math.LegacyDec) math.LegacyDec { return src.MulIntMut(math.NewInt(1)) }, + }, + "MulInt64Mut": { + fn: func(src math.LegacyDec) math.LegacyDec { return src.MulInt64Mut(1) }, + }, + "QuoMut": { + fn: func(src math.LegacyDec) math.LegacyDec { return src.QuoMut(math.LegacyNewDec(1)) }, + }, + "QuoTruncateMut": { + fn: func(src math.LegacyDec) math.LegacyDec { return src.QuoTruncateMut(math.LegacyNewDec(1)) }, + }, + "QuoRoundupMut": { + fn: func(src math.LegacyDec) math.LegacyDec { return src.QuoRoundupMut(math.LegacyNewDec(1)) }, + }, + } + for name, op := range ops { + t.Run(name, func(t *testing.T) { + if spec.expErr { + assert.Panics(t, func() { + got := op.fn(src) + t.Log(got.String()) + }) + return + } + exp := src.String() + // exp no panics + got := op.fn(src) + assert.Equal(t, exp, got.String()) + }) + } + }) + } +} + +func TestDecCeilLimits(t *testing.T) { + maxValid, ok := new(big.Int).SetString(maxValidDecNumber, 10) + require.True(t, ok) + minValid := new(big.Int).Neg(maxValid) + + specs := map[string]struct { + src *big.Int + exp string + expErr bool + }{ + "max": { + src: maxValid, + expErr: true, + }, + "max + 1": { + src: new(big.Int).Add(maxValid, big.NewInt(1)), + expErr: true, + }, + "max - 1e18, previous full number": { + src: new(big.Int).Sub(maxValid, new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil)), + exp: "115792089237316195423570985008687907853269984665640564039457584007913129639935.000000000000000000", + }, + "min": { + src: minValid, + exp: "-115792089237316195423570985008687907853269984665640564039457584007913129639935.000000000000000000", + }, + "min - 1": { + src: new(big.Int).Sub(minValid, big.NewInt(1)), + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + src := math.LegacyNewDecFromBigIntWithPrec(spec.src, 18) + if spec.expErr { + assert.Panics(t, func() { + got := src.Ceil() + t.Log(got.String()) + }) + return + } + got := src.Ceil() + assert.Equal(t, spec.exp, got.String()) + }) + } +} + +func TestTruncateIntLimits(t *testing.T) { + maxValid, ok := new(big.Int).SetString(maxValidDecNumber, 10) + require.True(t, ok) + minValid := new(big.Int).Neg(maxValid) + + specs := map[string]struct { + src *big.Int + exp string + expErr bool + }{ + "max": { + src: maxValid, + exp: "115792089237316195423570985008687907853269984665640564039457584007913129639935", + }, + "max + 1": { + src: new(big.Int).Add(maxValid, big.NewInt(1)), + expErr: true, + }, + "min": { + src: minValid, + exp: "-115792089237316195423570985008687907853269984665640564039457584007913129639935", + }, + "min - 1": { + src: new(big.Int).Sub(minValid, big.NewInt(1)), + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + src := math.LegacyNewDecFromBigIntWithPrec(spec.src, 18) + if spec.expErr { + assert.Panics(t, func() { + got := src.TruncateInt() + t.Log(got.String()) + }) + return + } + got := src.TruncateInt() + assert.Equal(t, spec.exp, got.String()) + }) + } +} + +func TestRoundIntLimits(t *testing.T) { + maxValid, ok := new(big.Int).SetString(maxValidDecNumber, 10) + require.True(t, ok) + minValid := new(big.Int).Neg(maxValid) + oneE18 := new(big.Int).Exp(big.NewInt(10), big.NewInt(18), nil) + + specs := map[string]struct { + src *big.Int + exp string + expErr bool + }{ + "max -1e18; previous full number": { + src: new(big.Int).Sub(maxValid, oneE18), + exp: "115792089237316195423570985008687907853269984665640564039457584007913129639935", + }, + "max": { + src: maxValid, + expErr: true, + }, + "max + 1": { + src: new(big.Int).Add(maxValid, big.NewInt(1)), + expErr: true, + }, + "min + 1e18; previous full number": { + src: new(big.Int).Add(minValid, oneE18), + exp: "-115792089237316195423570985008687907853269984665640564039457584007913129639935", + }, + "min": { + src: minValid, + expErr: true, + }, + "min - 1": { + src: new(big.Int).Sub(minValid, big.NewInt(1)), + expErr: true, + }, + } + for name, spec := range specs { + t.Run(name, func(t *testing.T) { + src := math.LegacyNewDecFromBigIntWithPrec(spec.src, 18) + t.Log(src.String()) + if spec.expErr { + assert.Panics(t, func() { + got := src.RoundInt() + t.Log(got.String()) + }) + return + } + got := src.RoundInt() + assert.Equal(t, spec.exp, got.String()) + }) + } +} + +func BenchmarkIsInValidRange(b *testing.B) { + maxValid, ok := new(big.Int).SetString(maxValidDecNumber, 10) + require.True(b, ok) + souceMax := math.LegacyNewDecFromBigIntWithPrec(maxValid, 18) + b.ResetTimer() + specs := map[string]math.LegacyDec{ + "max": souceMax, + "greater max": math.LegacyNewDecFromBigIntWithPrec(maxValid, 16), + "min": souceMax.Neg(), + "lower min": math.LegacyNewDecFromBigIntWithPrec(new(big.Int).Neg(maxValid), 16), + "zero": math.LegacyZeroDec(), + "one": math.LegacyOneDec(), + } + for name, source := range specs { + b.Run(name, func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = source.IsInValidRange() + } + }) + } +} diff --git a/math/go.mod b/math/go.mod index 53100ea6e7bb..c18586f7560b 100644 --- a/math/go.mod +++ b/math/go.mod @@ -18,3 +18,11 @@ require ( // Issue with math.Int{}.Size() implementation. retract [v1.1.0, v1.1.1] + +// Bit length differences between Int and Dec +retract ( + v1.3.0 + v1.2.0 + v1.1.2 + [v1.0.0, v1.0.1] +)