Refactor Gas/Fee Model (#3258)

This commit is contained in:
Alexander Bezobchuk
2019-01-18 08:45:20 -08:00
committed by Jack Zampolin
parent 8f7a222308
commit 36d1736a08
30 changed files with 785 additions and 249 deletions
+6 -39
View File
@@ -299,41 +299,6 @@ func (coins Coins) IsAllLTE(coinsB Coins) bool {
return coinsB.IsAllGTE(coins)
}
// IsAnyGTE returns true iff coins contains at least one denom that is present
// at a greater or equal amount in coinsB; it returns false otherwise.
//
// NOTE: IsAnyGTE operates under the invariant that coins are sorted by
// denominations.
func (coins Coins) IsAnyGTE(coinsB Coins) bool {
if len(coinsB) == 0 {
return false
}
j := 0
for _, coin := range coins {
searchOther := true // terminator in case coins breaks the sorted invariant
for j < len(coinsB) && searchOther {
switch strings.Compare(coin.Denom, coinsB[j].Denom) {
case -1:
// coin denom in less than the current other coin, so move to next coin
searchOther = false
case 0:
if coin.IsGTE(coinsB[j]) {
return true
}
fallthrough // skip to next other coin
case 1:
// coin denom is greater than the current other coin, so move to next other coin
j++
}
}
}
return false
}
// IsZero returns true if there are no coins or all coins are zero.
func (coins Coins) IsZero() bool {
for _, coin := range coins {
@@ -492,10 +457,12 @@ func (coins Coins) Sort() Coins {
var (
// Denominations can be 3 ~ 16 characters long.
reDnm = `[[:alpha:]][[:alnum:]]{2,15}`
reAmt = `[[:digit:]]+`
reSpc = `[[:space:]]*`
reCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reAmt, reSpc, reDnm))
reDnm = `[[:alpha:]][[:alnum:]]{2,15}`
reAmt = `[[:digit:]]+`
reDecAmt = `[[:digit:]]*\.[[:digit:]]+`
reSpc = `[[:space:]]*`
reCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reAmt, reSpc, reDnm))
reDecCoin = regexp.MustCompile(fmt.Sprintf(`^(%s)%s(%s)$`, reDecAmt, reSpc, reDnm))
)
// ParseCoin parses a cli input for one coin type, returning errors if invalid.
-16
View File
@@ -368,22 +368,6 @@ func TestCoinsLTE(t *testing.T) {
assert.True(t, Coins{}.IsAllLTE(Coins{{"a", one}}))
}
func TestCoinsIsAnyGTE(t *testing.T) {
one := NewInt(1)
two := NewInt(2)
assert.False(t, Coins{}.IsAnyGTE(Coins{}))
assert.False(t, Coins{{"a", one}}.IsAnyGTE(Coins{}))
assert.False(t, Coins{}.IsAnyGTE(Coins{{"a", one}}))
assert.False(t, Coins{{"a", one}}.IsAnyGTE(Coins{{"a", two}}))
assert.True(t, Coins{{"a", one}, {"b", two}}.IsAnyGTE(Coins{{"a", two}, {"b", one}}))
assert.True(t, Coins{{"a", one}}.IsAnyGTE(Coins{{"a", one}}))
assert.True(t, Coins{{"a", two}}.IsAnyGTE(Coins{{"a", one}}))
assert.True(t, Coins{{"a", one}}.IsAnyGTE(Coins{{"a", one}, {"b", two}}))
assert.True(t, Coins{{"a", one}, {"b", two}}.IsAnyGTE(Coins{{"a", one}, {"b", one}}))
assert.True(t, Coins{{"a", one}, {"b", one}}.IsAnyGTE(Coins{{"a", one}, {"b", two}}))
}
func TestParse(t *testing.T) {
one := NewInt(1)
+5 -5
View File
@@ -47,7 +47,7 @@ func NewContext(ms MultiStore, header abci.Header, isCheckTx bool, logger log.Lo
c = c.WithLogger(logger)
c = c.WithVoteInfos(nil)
c = c.WithGasMeter(NewInfiniteGasMeter())
c = c.WithMinimumFees(Coins{})
c = c.WithMinGasPrices(DecCoins{})
c = c.WithConsensusParams(nil)
return c
}
@@ -141,7 +141,7 @@ const (
contextKeyVoteInfos
contextKeyGasMeter
contextKeyBlockGasMeter
contextKeyMinimumFees
contextKeyMinGasPrices
contextKeyConsensusParams
)
@@ -169,7 +169,7 @@ func (c Context) BlockGasMeter() GasMeter { return c.Value(contextKeyBlockGasMet
func (c Context) IsCheckTx() bool { return c.Value(contextKeyIsCheckTx).(bool) }
func (c Context) MinimumFees() Coins { return c.Value(contextKeyMinimumFees).(Coins) }
func (c Context) MinGasPrices() DecCoins { return c.Value(contextKeyMinGasPrices).(DecCoins) }
func (c Context) ConsensusParams() *abci.ConsensusParams {
return c.Value(contextKeyConsensusParams).(*abci.ConsensusParams)
@@ -222,8 +222,8 @@ func (c Context) WithIsCheckTx(isCheckTx bool) Context {
return c.withValue(contextKeyIsCheckTx, isCheckTx)
}
func (c Context) WithMinimumFees(minFees Coins) Context {
return c.withValue(contextKeyMinimumFees, minFees)
func (c Context) WithMinGasPrices(gasPrices DecCoins) Context {
return c.withValue(contextKeyMinGasPrices, gasPrices)
}
func (c Context) WithConsensusParams(params *abci.ConsensusParams) Context {
+3 -3
View File
@@ -163,7 +163,7 @@ func TestContextWithCustom(t *testing.T) {
logger := NewMockLogger()
voteinfos := []abci.VoteInfo{{}}
meter := types.NewGasMeter(10000)
minFees := types.Coins{types.NewInt64Coin("feetoken", 1)}
minGasPrices := types.DecCoins{types.NewDecCoin("feetoken", 1)}
ctx = types.NewContext(nil, header, ischeck, logger)
require.Equal(t, header, ctx.BlockHeader())
@@ -174,7 +174,7 @@ func TestContextWithCustom(t *testing.T) {
WithTxBytes(txbytes).
WithVoteInfos(voteinfos).
WithGasMeter(meter).
WithMinimumFees(minFees)
WithMinGasPrices(minGasPrices)
require.Equal(t, height, ctx.BlockHeight())
require.Equal(t, chainid, ctx.ChainID())
require.Equal(t, ischeck, ctx.IsCheckTx())
@@ -182,5 +182,5 @@ func TestContextWithCustom(t *testing.T) {
require.Equal(t, logger, ctx.Logger())
require.Equal(t, voteinfos, ctx.VoteInfos())
require.Equal(t, meter, ctx.GasMeter())
require.Equal(t, minFees, types.Coins{types.NewInt64Coin("feetoken", 1)})
require.Equal(t, minGasPrices, ctx.MinGasPrices())
}
+169 -1
View File
@@ -2,9 +2,15 @@ package types
import (
"fmt"
"sort"
"strings"
"github.com/pkg/errors"
)
// ----------------------------------------------------------------------------
// Decimal Coin
// Coins which can have additional decimal points
type DecCoin struct {
Denom string `json:"denom"`
@@ -12,6 +18,13 @@ type DecCoin struct {
}
func NewDecCoin(denom string, amount int64) DecCoin {
if amount < 0 {
panic(fmt.Sprintf("negative decimal coin amount: %v\n", amount))
}
if strings.ToLower(denom) != denom {
panic(fmt.Sprintf("denom cannot contain upper case characters: %s\n", denom))
}
return DecCoin{
Denom: denom,
Amount: NewDec(amount),
@@ -19,6 +32,13 @@ func NewDecCoin(denom string, amount int64) DecCoin {
}
func NewDecCoinFromDec(denom string, amount Dec) DecCoin {
if amount.LT(ZeroDec()) {
panic(fmt.Sprintf("negative decimal coin amount: %v\n", amount))
}
if strings.ToLower(denom) != denom {
panic(fmt.Sprintf("denom cannot contain upper case characters: %s\n", denom))
}
return DecCoin{
Denom: denom,
Amount: amount,
@@ -26,6 +46,13 @@ func NewDecCoinFromDec(denom string, amount Dec) DecCoin {
}
func NewDecCoinFromCoin(coin Coin) DecCoin {
if coin.Amount.LT(ZeroInt()) {
panic(fmt.Sprintf("negative decimal coin amount: %v\n", coin.Amount))
}
if strings.ToLower(coin.Denom) != coin.Denom {
panic(fmt.Sprintf("denom cannot contain upper case characters: %s\n", coin.Denom))
}
return DecCoin{
Denom: coin.Denom,
Amount: NewDecFromInt(coin.Amount),
@@ -55,7 +82,21 @@ func (coin DecCoin) TruncateDecimal() (Coin, DecCoin) {
return NewCoin(coin.Denom, truncated), DecCoin{coin.Denom, change}
}
//_______________________________________________________________________
// IsPositive returns true if coin amount is positive.
//
// TODO: Remove once unsigned integers are used.
func (coin DecCoin) IsPositive() bool {
return coin.Amount.IsPositive()
}
// String implements the Stringer interface for DecCoin. It returns a
// human-readable representation of a decimal coin.
func (coin DecCoin) String() string {
return fmt.Sprintf("%v%v", coin.Amount, coin.Denom)
}
// ----------------------------------------------------------------------------
// Decimal Coins
// coins with decimal
type DecCoins []DecCoin
@@ -68,6 +109,21 @@ func NewDecCoins(coins Coins) DecCoins {
return dcs
}
// String implements the Stringer interface for DecCoins. It returns a
// human-readable representation of decimal coins.
func (coins DecCoins) String() string {
if len(coins) == 0 {
return ""
}
out := ""
for _, coin := range coins {
out += fmt.Sprintf("%v,", coin.String())
}
return out[:len(out)-1]
}
// return the coins with trunctated decimals, and return the change
func (coins DecCoins) TruncateDecimal() (Coins, DecCoins) {
changeSum := DecCoins{}
@@ -201,3 +257,115 @@ func (coins DecCoins) IsZero() bool {
}
return true
}
// IsValid asserts the DecCoins are sorted, have positive amount, and Denom
// does not contain upper case characters.
func (coins DecCoins) IsValid() bool {
switch len(coins) {
case 0:
return true
case 1:
if strings.ToLower(coins[0].Denom) != coins[0].Denom {
return false
}
return coins[0].IsPositive()
default:
// check single coin case
if !(DecCoins{coins[0]}).IsValid() {
return false
}
lowDenom := coins[0].Denom
for _, coin := range coins[1:] {
if strings.ToLower(coin.Denom) != coin.Denom {
return false
}
if coin.Denom <= lowDenom {
return false
}
if !coin.IsPositive() {
return false
}
// we compare each coin against the last denom
lowDenom = coin.Denom
}
return true
}
}
//-----------------------------------------------------------------------------
// Sorting
var _ sort.Interface = Coins{}
//nolint
func (coins DecCoins) Len() int { return len(coins) }
func (coins DecCoins) Less(i, j int) bool { return coins[i].Denom < coins[j].Denom }
func (coins DecCoins) Swap(i, j int) { coins[i], coins[j] = coins[j], coins[i] }
// Sort is a helper function to sort the set of decimal coins in-place.
func (coins DecCoins) Sort() DecCoins {
sort.Sort(coins)
return coins
}
// ----------------------------------------------------------------------------
// Parsing
// ParseDecCoin parses a decimal coin from a string, returning an error if
// invalid. An empty string is considered invalid.
func ParseDecCoin(coinStr string) (coin DecCoin, err error) {
coinStr = strings.TrimSpace(coinStr)
matches := reDecCoin.FindStringSubmatch(coinStr)
if matches == nil {
return DecCoin{}, fmt.Errorf("invalid decimal coin expression: %s", coinStr)
}
amountStr, denomStr := matches[1], matches[2]
amount, err := NewDecFromStr(amountStr)
if err != nil {
return DecCoin{}, errors.Wrap(err, fmt.Sprintf("failed to parse decimal coin amount: %s", amountStr))
}
if denomStr != strings.ToLower(denomStr) {
return DecCoin{}, fmt.Errorf("denom cannot contain upper case characters: %s", denomStr)
}
return NewDecCoinFromDec(denomStr, amount), nil
}
// ParseDecCoins will parse out a list of decimal coins separated by commas.
// If nothing is provided, it returns nil DecCoins. Returned decimal coins are
// sorted.
func ParseDecCoins(coinsStr string) (coins DecCoins, err error) {
coinsStr = strings.TrimSpace(coinsStr)
if len(coinsStr) == 0 {
return nil, nil
}
coinStrs := strings.Split(coinsStr, ",")
for _, coinStr := range coinStrs {
coin, err := ParseDecCoin(coinStr)
if err != nil {
return nil, err
}
coins = append(coins, coin)
}
// sort coins for determinism
coins.Sort()
// validate coins before returning
if !coins.IsValid() {
return nil, fmt.Errorf("parsed decimal coins are invalid: %#v", coins)
}
return coins, nil
}
+189 -12
View File
@@ -3,30 +3,80 @@ package types
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewDecCoin(t *testing.T) {
require.NotPanics(t, func() {
NewDecCoin("a", 5)
})
require.NotPanics(t, func() {
NewDecCoin("a", 0)
})
require.Panics(t, func() {
NewDecCoin("A", 5)
})
require.Panics(t, func() {
NewDecCoin("a", -5)
})
}
func TestNewDecCoinFromDec(t *testing.T) {
require.NotPanics(t, func() {
NewDecCoinFromDec("a", NewDec(5))
})
require.NotPanics(t, func() {
NewDecCoinFromDec("a", ZeroDec())
})
require.Panics(t, func() {
NewDecCoinFromDec("A", NewDec(5))
})
require.Panics(t, func() {
NewDecCoinFromDec("a", NewDec(-5))
})
}
func TestNewDecCoinFromCoin(t *testing.T) {
require.NotPanics(t, func() {
NewDecCoinFromCoin(Coin{"a", NewInt(5)})
})
require.NotPanics(t, func() {
NewDecCoinFromCoin(Coin{"a", NewInt(0)})
})
require.Panics(t, func() {
NewDecCoinFromCoin(Coin{"A", NewInt(5)})
})
require.Panics(t, func() {
NewDecCoinFromCoin(Coin{"a", NewInt(-5)})
})
}
func TestDecCoinIsPositive(t *testing.T) {
dc := NewDecCoin("a", 5)
require.True(t, dc.IsPositive())
dc = NewDecCoin("a", 0)
require.False(t, dc.IsPositive())
}
func TestPlusDecCoin(t *testing.T) {
decCoinA1 := DecCoin{"A", NewDecWithPrec(11, 1)}
decCoinA2 := DecCoin{"A", NewDecWithPrec(22, 1)}
decCoinB1 := DecCoin{"B", NewDecWithPrec(11, 1)}
decCoinA1 := NewDecCoinFromDec("a", NewDecWithPrec(11, 1))
decCoinA2 := NewDecCoinFromDec("a", NewDecWithPrec(22, 1))
decCoinB1 := NewDecCoinFromDec("b", NewDecWithPrec(11, 1))
// regular add
res := decCoinA1.Plus(decCoinA1)
require.Equal(t, decCoinA2, res, "sum of coins is incorrect")
// bad denom add
assert.Panics(t, func() {
require.Panics(t, func() {
decCoinA1.Plus(decCoinB1)
}, "expected panic on sum of different denoms")
}
func TestPlusDecCoins(t *testing.T) {
one := NewDec(1)
zero := NewDec(0)
negone := NewDec(-1)
two := NewDec(2)
cases := []struct {
@@ -34,11 +84,9 @@ func TestPlusDecCoins(t *testing.T) {
inputTwo DecCoins
expected DecCoins
}{
{DecCoins{{"A", one}, {"B", one}}, DecCoins{{"A", one}, {"B", one}}, DecCoins{{"A", two}, {"B", two}}},
{DecCoins{{"A", zero}, {"B", one}}, DecCoins{{"A", zero}, {"B", zero}}, DecCoins{{"B", one}}},
{DecCoins{{"A", zero}, {"B", zero}}, DecCoins{{"A", zero}, {"B", zero}}, DecCoins(nil)},
{DecCoins{{"A", one}, {"B", zero}}, DecCoins{{"A", negone}, {"B", zero}}, DecCoins(nil)},
{DecCoins{{"A", negone}, {"B", zero}}, DecCoins{{"A", zero}, {"B", zero}}, DecCoins{{"A", negone}}},
{DecCoins{{"a", one}, {"b", one}}, DecCoins{{"a", one}, {"b", one}}, DecCoins{{"a", two}, {"b", two}}},
{DecCoins{{"a", zero}, {"b", one}}, DecCoins{{"a", zero}, {"b", zero}}, DecCoins{{"b", one}}},
{DecCoins{{"a", zero}, {"b", zero}}, DecCoins{{"a", zero}, {"b", zero}}, DecCoins(nil)},
}
for tcIndex, tc := range cases {
@@ -46,3 +94,132 @@ func TestPlusDecCoins(t *testing.T) {
require.Equal(t, tc.expected, res, "sum of coins is incorrect, tc #%d", tcIndex)
}
}
func TestSortDecCoins(t *testing.T) {
good := DecCoins{
NewDecCoin("gas", 1),
NewDecCoin("mineral", 1),
NewDecCoin("tree", 1),
}
empty := DecCoins{
NewDecCoin("gold", 0),
}
badSort1 := DecCoins{
NewDecCoin("tree", 1),
NewDecCoin("gas", 1),
NewDecCoin("mineral", 1),
}
badSort2 := DecCoins{ // both are after the first one, but the second and third are in the wrong order
NewDecCoin("gas", 1),
NewDecCoin("tree", 1),
NewDecCoin("mineral", 1),
}
badAmt := DecCoins{
NewDecCoin("gas", 1),
NewDecCoin("tree", 0),
NewDecCoin("mineral", 1),
}
dup := DecCoins{
NewDecCoin("gas", 1),
NewDecCoin("gas", 1),
NewDecCoin("mineral", 1),
}
cases := []struct {
coins DecCoins
before, after bool // valid before/after sort
}{
{good, true, true},
{empty, false, false},
{badSort1, false, true},
{badSort2, false, true},
{badAmt, false, false},
{dup, false, false},
}
for tcIndex, tc := range cases {
require.Equal(t, tc.before, tc.coins.IsValid(), "coin validity is incorrect before sorting, tc #%d", tcIndex)
tc.coins.Sort()
require.Equal(t, tc.after, tc.coins.IsValid(), "coin validity is incorrect after sorting, tc #%d", tcIndex)
}
}
func TestDecCoinsIsValid(t *testing.T) {
testCases := []struct {
input DecCoins
expected bool
}{
{DecCoins{}, true},
{DecCoins{DecCoin{"a", NewDec(5)}}, true},
{DecCoins{DecCoin{"a", NewDec(5)}, DecCoin{"b", NewDec(100000)}}, true},
{DecCoins{DecCoin{"a", NewDec(-5)}}, false},
{DecCoins{DecCoin{"A", NewDec(5)}}, false},
{DecCoins{DecCoin{"a", NewDec(5)}, DecCoin{"B", NewDec(100000)}}, false},
{DecCoins{DecCoin{"a", NewDec(5)}, DecCoin{"b", NewDec(-100000)}}, false},
{DecCoins{DecCoin{"a", NewDec(-5)}, DecCoin{"b", NewDec(100000)}}, false},
{DecCoins{DecCoin{"A", NewDec(5)}, DecCoin{"b", NewDec(100000)}}, false},
}
for i, tc := range testCases {
res := tc.input.IsValid()
require.Equal(t, tc.expected, res, "unexpected result for test case #%d, input: %v", i, tc.input)
}
}
func TestParseDecCoins(t *testing.T) {
testCases := []struct {
input string
expectedResult DecCoins
expectedErr bool
}{
{"", nil, false},
{"4stake", nil, true},
{"5.5atom,4stake", nil, true},
{"0.0stake", nil, true},
{"0.004STAKE", nil, true},
{
"0.004stake",
DecCoins{NewDecCoinFromDec("stake", NewDecWithPrec(4000000000000000, Precision))},
false,
},
{
"5.04atom,0.004stake",
DecCoins{
NewDecCoinFromDec("atom", NewDecWithPrec(5040000000000000000, Precision)),
NewDecCoinFromDec("stake", NewDecWithPrec(4000000000000000, Precision)),
},
false,
},
}
for i, tc := range testCases {
res, err := ParseDecCoins(tc.input)
if tc.expectedErr {
require.Error(t, err, "expected error for test case #%d, input: %v", i, tc.input)
} else {
require.NoError(t, err, "unexpected error for test case #%d, input: %v", i, tc.input)
require.Equal(t, tc.expectedResult, res, "unexpected result for test case #%d, input: %v", i, tc.input)
}
}
}
func TestDecCoinsString(t *testing.T) {
testCases := []struct {
input DecCoins
expected string
}{
{DecCoins{}, ""},
{
DecCoins{
NewDecCoinFromDec("atom", NewDecWithPrec(5040000000000000000, Precision)),
NewDecCoinFromDec("stake", NewDecWithPrec(4000000000000000, Precision)),
},
"5.040000000000000000atom,0.004000000000000000stake",
},
}
for i, tc := range testCases {
out := tc.input.String()
require.Equal(t, tc.expected, out, "unexpected result for test case #%d, input: %v", i, tc.input)
}
}
+21 -1
View File
@@ -347,7 +347,7 @@ func chopPrecisionAndRound(d *big.Int) *big.Int {
return d
}
// get the trucated quotient and remainder
// get the truncated quotient and remainder
quo, rem := d, big.NewInt(0)
quo, rem = quo.QuoRem(d, precisionReuse, rem)
@@ -419,6 +419,26 @@ func (d Dec) TruncateDec() Dec {
return NewDecFromBigInt(chopPrecisionAndTruncateNonMutative(d.Int))
}
// Ceil returns the smallest interger value (as a decimal) that is greater than
// or equal to the given decimal.
func (d Dec) Ceil() Dec {
tmp := new(big.Int).Set(d.Int)
quo, rem := tmp, big.NewInt(0)
quo, rem = quo.QuoRem(tmp, precisionReuse, rem)
// no need to round with a zero remainder regardless of sign
if rem.Cmp(zeroInt) == 0 {
return NewDecFromBigInt(quo)
}
if rem.Sign() == -1 {
return NewDecFromBigInt(quo)
}
return NewDecFromBigInt(quo.Add(quo, oneInt))
}
//___________________________________________________________________________________
// reuse nil values
+21
View File
@@ -384,3 +384,24 @@ func TestDecMulInt(t *testing.T) {
require.Equal(t, tc.want, got, "Incorrect result on test case %d", i)
}
}
func TestDecCeil(t *testing.T) {
testCases := []struct {
input Dec
expected Dec
}{
{NewDecWithPrec(1000000000000000, Precision), NewDec(1)}, // 0.001 => 1.0
{NewDecWithPrec(-1000000000000000, Precision), ZeroDec()}, // -0.001 => 0.0
{ZeroDec(), ZeroDec()}, // 0.0 => 0.0
{NewDecWithPrec(900000000000000000, Precision), NewDec(1)}, // 0.9 => 1.0
{NewDecWithPrec(4001000000000000000, Precision), NewDec(5)}, // 4.001 => 5.0
{NewDecWithPrec(-4001000000000000000, Precision), NewDec(-4)}, // -4.001 => -4.0
{NewDecWithPrec(4700000000000000000, Precision), NewDec(5)}, // 4.7 => 5.0
{NewDecWithPrec(-4700000000000000000, Precision), NewDec(-4)}, // -4.7 => -4.0
}
for i, tc := range testCases {
res := tc.input.Ceil()
require.Equal(t, tc.expected, res, "unexpected result for test case %d, input: %v", i, tc.input)
}
}