* x/stake: Limit the size of rationals from user input This commit sets the maximum number of decimal points that can be passed in from messages. This is enforced on the validate basic of MsgBeginUnbonding and MsgBeginRedelegation. The cli has been updated to truncate the user input to the specified precision. This also updates types/rational to return big ints for Num() and Den(). Closes #887 * Switch NewFromDecimal to error instead of truncating
244 lines
7.2 KiB
Go
244 lines
7.2 KiB
Go
package types
|
|
|
|
import (
|
|
"fmt"
|
|
"math/big"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// "that's one big rat!"
|
|
// ______
|
|
// / / /\ \____oo
|
|
// __ /___...._____ _\o
|
|
// __| |_ |_
|
|
|
|
// NOTE: never use new(Rat) or else
|
|
// we will panic unmarshalling into the
|
|
// nil embedded big.Rat
|
|
type Rat struct {
|
|
*big.Rat `json:"rat"`
|
|
}
|
|
|
|
// nolint - common values
|
|
func ZeroRat() Rat { return Rat{big.NewRat(0, 1)} }
|
|
func OneRat() Rat { return Rat{big.NewRat(1, 1)} }
|
|
|
|
// New - create a new Rat from integers
|
|
func NewRat(Numerator int64, Denominator ...int64) Rat {
|
|
switch len(Denominator) {
|
|
case 0:
|
|
return Rat{big.NewRat(Numerator, 1)}
|
|
case 1:
|
|
return Rat{big.NewRat(Numerator, Denominator[0])}
|
|
default:
|
|
panic("improper use of New, can only have one denominator")
|
|
}
|
|
}
|
|
|
|
// create a rational from decimal string or integer string
|
|
// precision is the number of values after the decimal point which should be read
|
|
func NewRatFromDecimal(decimalStr string, prec int) (f Rat, err Error) {
|
|
// first extract any negative symbol
|
|
neg := false
|
|
if string(decimalStr[0]) == "-" {
|
|
neg = true
|
|
decimalStr = decimalStr[1:]
|
|
}
|
|
|
|
str := strings.Split(decimalStr, ".")
|
|
|
|
var numStr string
|
|
var denom int64 = 1
|
|
switch len(str) {
|
|
case 1:
|
|
if len(str[0]) == 0 {
|
|
return f, ErrUnknownRequest("not a decimal string")
|
|
}
|
|
numStr = str[0]
|
|
case 2:
|
|
if len(str[0]) == 0 || len(str[1]) == 0 {
|
|
return f, ErrUnknownRequest("not a decimal string")
|
|
}
|
|
if len(str[1]) > prec {
|
|
return f, ErrUnknownRequest("string has too many decimals")
|
|
}
|
|
numStr = str[0] + str[1]
|
|
len := int64(len(str[1]))
|
|
denom = new(big.Int).Exp(big.NewInt(10), big.NewInt(len), nil).Int64()
|
|
default:
|
|
return f, ErrUnknownRequest("not a decimal string")
|
|
}
|
|
|
|
num, errConv := strconv.Atoi(numStr)
|
|
if errConv != nil && strings.HasSuffix(errConv.Error(), "value out of range") {
|
|
// resort to big int, don't make this default option for efficiency
|
|
numBig, success := new(big.Int).SetString(numStr, 10)
|
|
if success != true {
|
|
return f, ErrUnknownRequest("not a decimal string")
|
|
}
|
|
|
|
if neg {
|
|
numBig.Neg(numBig)
|
|
}
|
|
|
|
return NewRatFromBigInt(numBig, big.NewInt(denom)), nil
|
|
} else if errConv != nil {
|
|
return f, ErrUnknownRequest("not a decimal string")
|
|
}
|
|
|
|
if neg {
|
|
num *= -1
|
|
}
|
|
|
|
return NewRat(int64(num), denom), nil
|
|
}
|
|
|
|
// NewRatFromBigInt constructs Rat from big.Int
|
|
func NewRatFromBigInt(num *big.Int, denom ...*big.Int) Rat {
|
|
switch len(denom) {
|
|
case 0:
|
|
return Rat{new(big.Rat).SetInt(num)}
|
|
case 1:
|
|
return Rat{new(big.Rat).SetFrac(num, denom[0])}
|
|
default:
|
|
panic("improper use of NewRatFromBigInt, can only have one denominator")
|
|
}
|
|
}
|
|
|
|
// NewRatFromInt constructs Rat from Int
|
|
func NewRatFromInt(num Int, denom ...Int) Rat {
|
|
switch len(denom) {
|
|
case 0:
|
|
return Rat{new(big.Rat).SetInt(num.BigInt())}
|
|
case 1:
|
|
return Rat{new(big.Rat).SetFrac(num.BigInt(), denom[0].BigInt())}
|
|
default:
|
|
panic("improper use of NewRatFromBigInt, can only have one denominator")
|
|
}
|
|
}
|
|
|
|
//nolint
|
|
func (r Rat) Num() Int { return Int{r.Rat.Num()} } // Num - return the numerator
|
|
func (r Rat) Denom() Int { return Int{r.Rat.Denom()} } // Denom - return the denominator
|
|
func (r Rat) IsZero() bool { return r.Num().IsZero() } // IsZero - Is the Rat equal to zero
|
|
func (r Rat) Equal(r2 Rat) bool { return (r.Rat).Cmp(r2.Rat) == 0 }
|
|
func (r Rat) GT(r2 Rat) bool { return (r.Rat).Cmp(r2.Rat) == 1 } // greater than
|
|
func (r Rat) GTE(r2 Rat) bool { return !r.LT(r2) } // greater than or equal
|
|
func (r Rat) LT(r2 Rat) bool { return (r.Rat).Cmp(r2.Rat) == -1 } // less than
|
|
func (r Rat) LTE(r2 Rat) bool { return !r.GT(r2) } // less than or equal
|
|
func (r Rat) Mul(r2 Rat) Rat { return Rat{new(big.Rat).Mul(r.Rat, r2.Rat)} } // Mul - multiplication
|
|
func (r Rat) Quo(r2 Rat) Rat { return Rat{new(big.Rat).Quo(r.Rat, r2.Rat)} } // Quo - quotient
|
|
func (r Rat) Add(r2 Rat) Rat { return Rat{new(big.Rat).Add(r.Rat, r2.Rat)} } // Add - addition
|
|
func (r Rat) Sub(r2 Rat) Rat { return Rat{new(big.Rat).Sub(r.Rat, r2.Rat)} } // Sub - subtraction
|
|
func (r Rat) String() string { return r.Rat.String() }
|
|
func (r Rat) FloatString() string { return r.Rat.FloatString(10) } // a human-friendly string format. The last digit is rounded to nearest, with halves rounded away from zero.
|
|
|
|
var (
|
|
zero = big.NewInt(0)
|
|
one = big.NewInt(1)
|
|
two = big.NewInt(2)
|
|
five = big.NewInt(5)
|
|
nFive = big.NewInt(-5)
|
|
ten = big.NewInt(10)
|
|
)
|
|
|
|
// evaluate the rational using bankers rounding
|
|
func (r Rat) EvaluateBig() *big.Int {
|
|
|
|
num := r.Rat.Num()
|
|
denom := r.Rat.Denom()
|
|
|
|
d, rem := new(big.Int), new(big.Int)
|
|
d.QuoRem(num, denom, rem)
|
|
if rem.Cmp(zero) == 0 { // is the remainder zero
|
|
return d
|
|
}
|
|
|
|
// evaluate the remainder using bankers rounding
|
|
tenNum := new(big.Int).Mul(num, ten)
|
|
tenD := new(big.Int).Mul(d, ten)
|
|
remainderDigit := new(big.Int).Sub(new(big.Int).Quo(tenNum, denom), tenD) // get the first remainder digit
|
|
isFinalDigit := (new(big.Int).Rem(tenNum, denom).Cmp(zero) == 0) // is this the final digit in the remainder?
|
|
|
|
switch {
|
|
case isFinalDigit && (remainderDigit.Cmp(five) == 0 || remainderDigit.Cmp(nFive) == 0):
|
|
dRem2 := new(big.Int).Rem(d, two)
|
|
return new(big.Int).Add(d, dRem2) // always rounds to the even number
|
|
case remainderDigit.Cmp(five) != -1: //remainderDigit >= 5:
|
|
d.Add(d, one)
|
|
case remainderDigit.Cmp(nFive) != 1: //remainderDigit <= -5:
|
|
d.Sub(d, one)
|
|
}
|
|
return d
|
|
}
|
|
|
|
// evaluate the rational using bankers rounding
|
|
func (r Rat) Evaluate() int64 {
|
|
return r.EvaluateBig().Int64()
|
|
}
|
|
|
|
// EvaulateInt evaludates the rational using EvaluateBig
|
|
func (r Rat) EvaluateInt() Int {
|
|
return NewIntFromBigInt(r.EvaluateBig())
|
|
}
|
|
|
|
// round Rat with the provided precisionFactor
|
|
func (r Rat) Round(precisionFactor int64) Rat {
|
|
rTen := Rat{new(big.Rat).Mul(r.Rat, big.NewRat(precisionFactor, 1))}
|
|
return Rat{big.NewRat(rTen.Evaluate(), precisionFactor)}
|
|
}
|
|
|
|
// TODO panic if negative or if totalDigits < len(initStr)???
|
|
// evaluate as an integer and return left padded string
|
|
func (r Rat) ToLeftPadded(totalDigits int8) string {
|
|
intStr := r.EvaluateBig().String()
|
|
fcode := `%0` + strconv.Itoa(int(totalDigits)) + `s`
|
|
return fmt.Sprintf(fcode, intStr)
|
|
}
|
|
|
|
//___________________________________________________________________________________
|
|
|
|
//Wraps r.MarshalText().
|
|
func (r Rat) MarshalAmino() (string, error) {
|
|
if r.Rat == nil {
|
|
r.Rat = new(big.Rat)
|
|
}
|
|
bz, err := r.Rat.MarshalText()
|
|
return string(bz), err
|
|
}
|
|
|
|
// Requires a valid JSON string - strings quotes and calls UnmarshalText
|
|
func (r *Rat) UnmarshalAmino(text string) (err error) {
|
|
tempRat := big.NewRat(0, 1)
|
|
err = tempRat.UnmarshalText([]byte(text))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Rat = tempRat
|
|
return nil
|
|
}
|
|
|
|
//___________________________________________________________________________________
|
|
// helpers
|
|
|
|
// test if two rat arrays are the equal
|
|
func RatsEqual(r1s, r2s []Rat) bool {
|
|
if len(r1s) != len(r2s) {
|
|
return false
|
|
}
|
|
|
|
for i, r1 := range r1s {
|
|
if !r1.Equal(r2s[i]) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// intended to be used with require/assert: require.True(RatEq(...))
|
|
func RatEq(t *testing.T, exp, got Rat) (*testing.T, bool, string, Rat, Rat) {
|
|
return t, exp.Equal(got), "expected:\t%v\ngot:\t\t%v", exp, got
|
|
}
|