blockly minting (#2825)

* update mechanism to use average block time
* correctly sets accum height for zero-delegations
* update Decimal Format()
* clip withdrawal tokens
* PositiveDelegationInvariant
* DelegatorSharesInvariant
* DelAccumInvariants
This commit is contained in:
frog power 4000 2018-11-26 07:13:47 -05:00 committed by Jae Kwon
parent 4c36b0fe05
commit 7cb1ba625e
21 changed files with 453 additions and 69 deletions

View File

@ -14,6 +14,7 @@ BREAKING CHANGES
* [cli] [\#2875](https://github.com/cosmos/cosmos-sdk/pull/2875) Refactor `gaiad gentx` and avoid redirection to `gaiacli tx sign` for tx signing.
* Gaia
* [mint] [\#2825] minting now occurs every block, inflation parameter updates still hourly
* SDK
* [\#2752](https://github.com/cosmos/cosmos-sdk/pull/2752) Don't hardcode bondable denom.
@ -74,6 +75,7 @@ IMPROVEMENTS
- #2821 Codespaces are now strings
- #2779 Introduce `ValidateBasic` to the `Tx` interface and call it in the ante
handler.
- #2825 More staking and distribution invariants
* Tendermint
- #2796 Update to go-amino 0.14.1

View File

@ -59,7 +59,9 @@ func appStateFn(r *rand.Rand, accs []simulation.Account) json.RawMessage {
if numInitiallyBonded > numAccs {
numInitiallyBonded = numAccs
}
fmt.Printf("Selected randomly generated parameters for simulated genesis: {amount of steak per account: %v, initially bonded validators: %v}\n", amount, numInitiallyBonded)
fmt.Printf("Selected randomly generated parameters for simulated genesis:\n"+
"\t{amount of steak per account: %v, initially bonded validators: %v}\n",
amount, numInitiallyBonded)
// Randomly generate some genesis accounts
for _, acc := range accs {
@ -86,7 +88,8 @@ func appStateFn(r *rand.Rand, accs []simulation.Account) json.RawMessage {
GovernancePenalty: sdk.NewDecWithPrec(1, 2),
},
}
fmt.Printf("Selected randomly generated governance parameters: %+v\n", govGenesis)
fmt.Printf("Selected randomly generated governance parameters:\n\t%+v\n", govGenesis)
stakeGenesis := stake.GenesisState{
Pool: stake.InitialPool(),
Params: stake.Params{
@ -95,7 +98,8 @@ func appStateFn(r *rand.Rand, accs []simulation.Account) json.RawMessage {
BondDenom: stakeTypes.DefaultBondDenom,
},
}
fmt.Printf("Selected randomly generated staking parameters: %+v\n", stakeGenesis)
fmt.Printf("Selected randomly generated staking parameters:\n\t%+v\n", stakeGenesis)
slashingGenesis := slashing.GenesisState{
Params: slashing.Params{
MaxEvidenceAge: stakeGenesis.Params.UnbondingTime,
@ -107,21 +111,21 @@ func appStateFn(r *rand.Rand, accs []simulation.Account) json.RawMessage {
SlashFractionDowntime: sdk.NewDec(1).Quo(sdk.NewDec(int64(r.Intn(200) + 1))),
},
}
fmt.Printf("Selected randomly generated slashing parameters: %+v\n", slashingGenesis)
fmt.Printf("Selected randomly generated slashing parameters:\n\t%+v\n", slashingGenesis)
mintGenesis := mint.GenesisState{
Minter: mint.Minter{
InflationLastTime: time.Unix(0, 0),
Inflation: sdk.NewDecWithPrec(int64(r.Intn(99)), 2),
},
Params: mint.Params{
MintDenom: stakeTypes.DefaultBondDenom,
InflationRateChange: sdk.NewDecWithPrec(int64(r.Intn(99)), 2),
InflationMax: sdk.NewDecWithPrec(20, 2),
InflationMin: sdk.NewDecWithPrec(7, 2),
GoalBonded: sdk.NewDecWithPrec(67, 2),
},
Minter: mint.InitialMinter(
sdk.NewDecWithPrec(int64(r.Intn(99)), 2)),
Params: mint.NewParams(
stakeTypes.DefaultBondDenom,
sdk.NewDecWithPrec(int64(r.Intn(99)), 2),
sdk.NewDecWithPrec(20, 2),
sdk.NewDecWithPrec(7, 2),
sdk.NewDecWithPrec(67, 2),
uint64(60*60*8766/5)),
}
fmt.Printf("Selected randomly generated minting parameters: %v\n", mintGenesis)
fmt.Printf("Selected randomly generated minting parameters:\n\t%+v\n", mintGenesis)
var validators []stake.Validator
var delegations []stake.Delegation

View File

@ -1,18 +1,18 @@
# Begin-Block
## Inflation
Inflation occurs at the beginning of each block, however minting parameters
are only calculated once per hour.
Inflation occurs at the beginning of each block.
## NextInflationRate
### NextInflation
The target annual inflation rate is recalculated at the first block of each new
hour. The inflation is also subject to a rate change (positive or negative)
depending on the distance from the desired ratio (67%). The maximum rate change
possible is defined to be 13% per year, however the annual inflation is capped
as between 7% and 20%.
The target annual inflation rate is recalculated for each provisions cycle. The
inflation is also subject to a rate change (positive or negative) depending on
the distance from the desired ratio (67%). The maximum rate change possible is
defined to be 13% per year, however the annual inflation is capped as between
7% and 20%.
NextInflation(params Params, bondedRatio sdk.Dec) (inflation sdk.Dec) {
```
NextInflationRate(params Params, bondedRatio sdk.Dec) (inflation sdk.Dec) {
inflationRateChangePerYear = (1 - bondedRatio/params.GoalBonded) * params.InflationRateChange
inflationRateChange = inflationRateChangePerYear/hrsPerYr
@ -26,3 +26,25 @@ NextInflation(params Params, bondedRatio sdk.Dec) (inflation sdk.Dec) {
}
return inflation
```
## NextAnnualProvisions
Calculate the annual provisions based on current total supply and inflation
rate. This parameter is calculated once per block.
```
NextAnnualProvisions(params Params, totalSupply sdk.Dec) (provisions sdk.Dec) {
return Inflation * totalSupply
```
## BlockProvision
Calculate the provisions generated for each block based on current
annual provisions
```
BlockProvision(params Params) sdk.Coin {
provisionAmt = AnnualProvisions/ params.BlocksPerYear
return sdk.NewCoin(params.MintDenom, provisionAmt.Truncate())
```

View File

@ -8,8 +8,9 @@ The minter is a space for holding current inflation information.
```golang
type Minter struct {
InflationLastTime time.Time // block time which the last inflation was processed
Inflation sdk.Dec // current annual inflation rate
LastUpdate time.Time // time which the last update was made to the minter
Inflation sdk.Dec // current annual inflation rate
AnnualProvisions sdk.Dec // current annual exptected provisions
}
```
@ -26,6 +27,7 @@ type Params struct {
InflationMax sdk.Dec // maximum inflation rate
InflationMin sdk.Dec // minimum inflation rate
GoalBonded sdk.Dec // goal of percent bonded atoms
BlocksPerYear uint64 // expected blocks per year
}
```

View File

@ -28,7 +28,7 @@ type Coin struct {
// the amount is negative.
func NewCoin(denom string, amount Int) Coin {
if amount.LT(ZeroInt()) {
panic("negative coin amount")
panic(fmt.Sprintf("negative coin amount: %v\n", amount))
}
return Coin{

View File

@ -172,10 +172,21 @@ func NewDecFromStr(str string) (d Dec, err Error) {
return Dec{combined}, nil
}
// Decimal from string, panic on error
func MustNewDecFromStr(s string) Dec {
dec, err := NewDecFromStr(s)
if err != nil {
panic(err)
}
return dec
}
//______________________________________________________________________________________________
//nolint
func (d Dec) IsNil() bool { return d.Int == nil } // is decimal nil
func (d Dec) IsZero() bool { return (d.Int).Sign() == 0 } // is equal to zero
func (d Dec) IsNegative() bool { return (d.Int).Sign() == -1 } // is negative
func (d Dec) IsPositive() bool { return (d.Int).Sign() == 1 } // is positive
func (d Dec) Equal(d2 Dec) bool { return (d.Int).Cmp(d2.Int) == 0 } // equal decimals
func (d Dec) GT(d2 Dec) bool { return (d.Int).Cmp(d2.Int) > 0 } // greater than
func (d Dec) GTE(d2 Dec) bool { return (d.Int).Cmp(d2.Int) >= 0 } // greater than or equal
@ -252,6 +263,14 @@ func (d Dec) IsInteger() bool {
return new(big.Int).Rem(d.Int, precisionReuse).Sign() == 0
}
// format decimal state
func (d Dec) Format(s fmt.State, verb rune) {
_, err := s.Write([]byte(d.String()))
if err != nil {
panic(err)
}
}
func (d Dec) String() string {
bz, err := d.Int.MarshalText()
if err != nil {

View File

@ -10,6 +10,11 @@ import (
// Create a new validator distribution record
func (k Keeper) onValidatorCreated(ctx sdk.Context, valAddr sdk.ValAddress) {
// defensive check for existence
if k.HasValidatorDistInfo(ctx, valAddr) {
panic("validator dist info already exists (not cleaned up properly)")
}
height := ctx.BlockHeight()
vdi := types.ValidatorDistInfo{
OperatorAddr: valAddr,

View File

@ -162,7 +162,8 @@ func (fck DummyFeeCollectionKeeper) ClearCollectedFees(_ sdk.Context) {
//__________________________________________________________________________________
// used in simulation
// iterate over all the validator distribution infos (inefficient, just used to check invariants)
// iterate over all the validator distribution infos (inefficient, just used to
// check invariants)
func (k Keeper) IterateValidatorDistInfos(ctx sdk.Context,
fn func(index int64, distInfo types.ValidatorDistInfo) (stop bool)) {
@ -179,3 +180,22 @@ func (k Keeper) IterateValidatorDistInfos(ctx sdk.Context,
index++
}
}
// iterate over all the delegation distribution infos (inefficient, just used
// to check invariants)
func (k Keeper) IterateDelegationDistInfos(ctx sdk.Context,
fn func(index int64, distInfo types.DelegationDistInfo) (stop bool)) {
store := ctx.KVStore(k.storeKey)
iter := sdk.KVStorePrefixIterator(store, DelegationDistInfoKey)
defer iter.Close()
index := int64(0)
for ; iter.Valid(); iter.Next() {
var ddi types.DelegationDistInfo
k.cdc.MustUnmarshalBinaryLengthPrefixed(iter.Value(), &ddi)
if fn(index, ddi) {
return
}
index++
}
}

View File

@ -36,6 +36,13 @@ func (k Keeper) SetValidatorDistInfo(ctx sdk.Context, vdi types.ValidatorDistInf
// remove a validator distribution info
func (k Keeper) RemoveValidatorDistInfo(ctx sdk.Context, valAddr sdk.ValAddress) {
// defensive check
vdi := k.GetValidatorDistInfo(ctx, valAddr)
if vdi.DelAccum.Accum.IsPositive() {
panic("Should not delete validator with unwithdrawn delegator accum")
}
store := ctx.KVStore(k.storeKey)
store.Delete(GetValidatorDistInfoKey(valAddr))
}

View File

@ -18,6 +18,10 @@ func AllInvariants(d distr.Keeper, sk distr.StakeKeeper) simulation.Invariant {
if err != nil {
return err
}
err = DelAccumInvariants(d, sk)(app)
if err != nil {
return err
}
return nil
}
}
@ -48,3 +52,81 @@ func ValAccumInvariants(k distr.Keeper, sk distr.StakeKeeper) simulation.Invaria
return nil
}
}
// DelAccumInvariants checks that each validator del accum == sum all delegators' accum
func DelAccumInvariants(k distr.Keeper, sk distr.StakeKeeper) simulation.Invariant {
return func(app *baseapp.BaseApp) error {
mockHeader := abci.Header{Height: app.LastBlockHeight() + 1}
ctx := app.NewContext(false, mockHeader)
height := ctx.BlockHeight()
totalDelAccumFromVal := make(map[string]sdk.Dec) // key is the valOpAddr string
totalDelAccum := make(map[string]sdk.Dec)
// iterate the validators
iterVal := func(_ int64, vdi distr.ValidatorDistInfo) bool {
key := vdi.OperatorAddr.String()
validator := sk.Validator(ctx, vdi.OperatorAddr)
totalDelAccumFromVal[key] = vdi.GetTotalDelAccum(height,
validator.GetDelegatorShares())
// also initialize the delegation map
totalDelAccum[key] = sdk.ZeroDec()
return false
}
k.IterateValidatorDistInfos(ctx, iterVal)
// iterate the delegations
iterDel := func(_ int64, ddi distr.DelegationDistInfo) bool {
key := ddi.ValOperatorAddr.String()
delegation := sk.Delegation(ctx, ddi.DelegatorAddr, ddi.ValOperatorAddr)
totalDelAccum[key] = totalDelAccum[key].Add(
ddi.GetDelAccum(height, delegation.GetShares()))
return false
}
k.IterateDelegationDistInfos(ctx, iterDel)
// compare
for key, delAccumFromVal := range totalDelAccumFromVal {
sumDelAccum := totalDelAccum[key]
if !sumDelAccum.Equal(delAccumFromVal) {
logDelAccums := ""
iterDel := func(_ int64, ddi distr.DelegationDistInfo) bool {
keyLog := ddi.ValOperatorAddr.String()
if keyLog == key {
delegation := sk.Delegation(ctx, ddi.DelegatorAddr, ddi.ValOperatorAddr)
accum := ddi.GetDelAccum(height, delegation.GetShares())
if accum.IsPositive() {
logDelAccums += fmt.Sprintf("\n\t\tdel: %v, accum: %v",
ddi.DelegatorAddr.String(),
accum.String())
}
}
return false
}
k.IterateDelegationDistInfos(ctx, iterDel)
operAddr, err := sdk.ValAddressFromBech32(key)
if err != nil {
panic(err)
}
validator := sk.Validator(ctx, operAddr)
return fmt.Errorf("delegator accum invariance: \n"+
"\tvalidator key: %v\n"+
"\tvalidator: %+v\n"+
"\tsum delegator accum: %v\n"+
"\tvalidator's total delegator accum: %v\n"+
"\tlog of delegations with accum: %v\n",
key, validator, sumDelAccum.String(),
delAccumFromVal.String(), logDelAccums)
}
}
return nil
}
}

View File

@ -20,6 +20,13 @@ func NewDecCoin(denom string, amount int64) DecCoin {
}
}
func NewDecCoinFromDec(denom string, amount sdk.Dec) DecCoin {
return DecCoin{
Denom: denom,
Amount: amount,
}
}
func NewDecCoinFromCoin(coin sdk.Coin) DecCoin {
return DecCoin{
Denom: coin.Denom,
@ -140,7 +147,7 @@ func (coins DecCoins) MulDec(d sdk.Dec) DecCoins {
return res
}
// divide all the coins by a multiple
// divide all the coins by a decimal
func (coins DecCoins) QuoDec(d sdk.Dec) DecCoins {
res := make([]DecCoin, len(coins))
for i, coin := range coins {
@ -176,3 +183,13 @@ func (coins DecCoins) AmountOf(denom string) sdk.Dec {
}
}
}
// returns the amount of a denom from deccoins
func (coins DecCoins) HasNegative() bool {
for _, coin := range coins {
if coin.Amount.IsNegative() {
return true
}
}
return false
}

View File

@ -1,6 +1,8 @@
package types
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
@ -24,7 +26,17 @@ func NewDelegationDistInfo(delegatorAddr sdk.AccAddress, valOperatorAddr sdk.Val
// Get the calculated accum of this delegator at the provided height
func (di DelegationDistInfo) GetDelAccum(height int64, delegatorShares sdk.Dec) sdk.Dec {
blocks := height - di.DelPoolWithdrawalHeight
return delegatorShares.MulInt(sdk.NewInt(blocks))
accum := delegatorShares.MulInt(sdk.NewInt(blocks))
// defensive check
if accum.IsNegative() {
panic(fmt.Sprintf("negative accum: %v\n"+
"\theight: %v\n"+
"\tdelegation_dist_info: %v\n"+
"\tdelegator_shares: %v\n",
accum.String(), height, di, delegatorShares))
}
return accum
}
// Withdraw rewards from delegator.
@ -41,7 +53,9 @@ func (di DelegationDistInfo) WithdrawRewards(wc WithdrawContext, vi ValidatorDis
fp := wc.FeePool
vi = vi.UpdateTotalDelAccum(wc.Height, totalDelShares)
// Break out to prevent a divide by zero.
if vi.DelAccum.Accum.IsZero() {
di.DelPoolWithdrawalHeight = wc.Height
return di, vi, fp, DecCoins{}
}
@ -49,9 +63,43 @@ func (di DelegationDistInfo) WithdrawRewards(wc WithdrawContext, vi ValidatorDis
accum := di.GetDelAccum(wc.Height, delegatorShares)
di.DelPoolWithdrawalHeight = wc.Height
withdrawalTokens := vi.DelPool.MulDec(accum).QuoDec(vi.DelAccum.Accum)
vi.DelPool = vi.DelPool.Minus(withdrawalTokens)
// Clip withdrawal tokens by pool, due to possible rounding errors.
// This rounding error may be introduced upon multiplication since
// we're clipping decimal digits, and then when we divide by a number ~1 or
// < 1, the error doesn't get "buried", and if << 1 it'll get amplified.
// more: https://github.com/cosmos/cosmos-sdk/issues/2888#issuecomment-441387987
for i, decCoin := range withdrawalTokens {
poolDenomAmount := vi.DelPool.AmountOf(decCoin.Denom)
if decCoin.Amount.GT(poolDenomAmount) {
withdrawalTokens[i] = NewDecCoinFromDec(decCoin.Denom, poolDenomAmount)
}
}
// defensive check for impossible accum ratios
if accum.GT(vi.DelAccum.Accum) {
panic(fmt.Sprintf("accum > vi.DelAccum.Accum:\n"+
"\taccum\t\t\t%v\n"+
"\tvi.DelAccum.Accum\t%v\n",
accum, vi.DelAccum.Accum))
}
remDelPool := vi.DelPool.Minus(withdrawalTokens)
// defensive check
if remDelPool.HasNegative() {
panic(fmt.Sprintf("negative remDelPool: %v\n"+
"\tvi.DelPool\t\t%v\n"+
"\taccum\t\t\t%v\n"+
"\tvi.DelAccum.Accum\t%v\n"+
"\twithdrawalTokens\t%v\n",
remDelPool, vi.DelPool, accum,
vi.DelAccum.Accum, withdrawalTokens))
}
vi.DelPool = remDelPool
vi.DelAccum.Accum = vi.DelAccum.Accum.Sub(accum)
return di, vi, fp, withdrawalTokens

View File

@ -6,21 +6,26 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Called every block, process inflation on the first block of every hour
// Inflate every block, update inflation parameters once per hour
func BeginBlocker(ctx sdk.Context, k Keeper) {
blockTime := ctx.BlockHeader().Time
minter := k.GetMinter(ctx)
if blockTime.Sub(minter.InflationLastTime) < time.Hour { // only mint on the hour!
params := k.GetParams(ctx)
mintedCoin := minter.BlockProvision(params)
k.fck.AddCollectedFees(ctx, sdk.Coins{mintedCoin})
k.sk.InflateSupply(ctx, sdk.NewDecFromInt(mintedCoin.Amount))
if blockTime.Sub(minter.LastUpdate) < time.Hour {
return
}
params := k.GetParams(ctx)
// adjust the inflation, hourly-provision rate every hour
totalSupply := k.sk.TotalPower(ctx)
bondedRatio := k.sk.BondedRatio(ctx)
minter.InflationLastTime = blockTime
minter, mintedCoin := minter.ProcessProvisions(params, totalSupply, bondedRatio)
k.fck.AddCollectedFees(ctx, sdk.Coins{mintedCoin})
k.sk.InflateSupply(ctx, sdk.NewDecFromInt(mintedCoin.Amount))
minter.Inflation = minter.NextInflationRate(params, bondedRatio)
minter.AnnualProvisions = minter.NextAnnualProvisions(params, totalSupply)
minter.LastUpdate = blockTime
k.SetMinter(ctx, minter)
}

View File

@ -20,7 +20,7 @@ func NewGenesisState(minter Minter, params Params) GenesisState {
// get raw genesis raw message for testing
func DefaultGenesisState() GenesisState {
return GenesisState{
Minter: InitialMinter(),
Minter: DefaultInitialMinter(),
Params: DefaultParams(),
}
}

View File

@ -7,45 +7,54 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// current inflation state
// Minter represents the minting state
type Minter struct {
InflationLastTime time.Time `json:"inflation_last_time"` // block time which the last inflation was processed
Inflation sdk.Dec `json:"inflation"` // current annual inflation rate
LastUpdate time.Time `json:"last_update"` // time which the last update was made to the minter
Inflation sdk.Dec `json:"inflation"` // current annual inflation rate
AnnualProvisions sdk.Dec `json:"annual_provisions"` // current annual expected provisions
}
// minter object for a new minter
func InitialMinter() Minter {
// Create a new minter object
func NewMinter(lastUpdate time.Time, inflation,
annualProvisions sdk.Dec) Minter {
return Minter{
InflationLastTime: time.Unix(0, 0),
Inflation: sdk.NewDecWithPrec(13, 2),
LastUpdate: lastUpdate,
Inflation: inflation,
AnnualProvisions: annualProvisions,
}
}
// minter object for a new chain
func InitialMinter(inflation sdk.Dec) Minter {
return NewMinter(
time.Unix(0, 0),
inflation,
sdk.NewDec(0),
)
}
// default initial minter object for a new chain
// which uses an inflation rate of 13%
func DefaultInitialMinter() Minter {
return InitialMinter(
sdk.NewDecWithPrec(13, 2),
)
}
func validateMinter(minter Minter) error {
if minter.Inflation.LT(sdk.ZeroDec()) {
return fmt.Errorf("mint parameter Inflation should be positive, is %s ", minter.Inflation.String())
}
if minter.Inflation.GT(sdk.OneDec()) {
return fmt.Errorf("mint parameter Inflation must be <= 1, is %s", minter.Inflation.String())
return fmt.Errorf("mint parameter Inflation should be positive, is %s",
minter.Inflation.String())
}
return nil
}
var hrsPerYr = sdk.NewDec(8766) // as defined by a julian year of 365.25 days
// process provisions for an hour period
func (m Minter) ProcessProvisions(params Params, totalSupply, bondedRatio sdk.Dec) (
minter Minter, provisions sdk.Coin) {
m.Inflation = m.NextInflation(params, bondedRatio)
provisionsDec := m.Inflation.Mul(totalSupply).Quo(hrsPerYr)
provisions = sdk.NewCoin(params.MintDenom, provisionsDec.TruncateInt())
return m, provisions
}
// get the next inflation rate for the hour
func (m Minter) NextInflation(params Params, bondedRatio sdk.Dec) (inflation sdk.Dec) {
// get the new inflation rate for the next hour
func (m Minter) NextInflationRate(params Params, bondedRatio sdk.Dec) (
inflation sdk.Dec) {
// The target annual inflation rate is recalculated for each previsions cycle. The
// inflation is also subject to a rate change (positive or negative) depending on
@ -70,3 +79,16 @@ func (m Minter) NextInflation(params Params, bondedRatio sdk.Dec) (inflation sdk
return inflation
}
// calculate the annual provisions based on current total supply and inflation rate
func (m Minter) NextAnnualProvisions(params Params, totalSupply sdk.Dec) (
provisions sdk.Dec) {
return m.Inflation.Mul(totalSupply)
}
// get the provisions for a block based on the annual provisions rate
func (m Minter) BlockProvision(params Params) sdk.Coin {
provisionAmt := m.AnnualProvisions.QuoInt(sdk.NewInt(int64(params.BlocksPerYear)))
return sdk.NewCoin(params.MintDenom, provisionAmt.TruncateInt())
}

View File

@ -1,6 +1,7 @@
package mint
import (
"math/rand"
"testing"
"github.com/stretchr/testify/require"
@ -9,7 +10,7 @@ import (
)
func TestNextInflation(t *testing.T) {
minter := InitialMinter()
minter := DefaultInitialMinter()
params := DefaultParams()
// Governing Mechanism:
@ -44,10 +45,57 @@ func TestNextInflation(t *testing.T) {
for i, tc := range tests {
minter.Inflation = tc.setInflation
inflation := minter.NextInflation(params, tc.bondedRatio)
inflation := minter.NextInflationRate(params, tc.bondedRatio)
diffInflation := inflation.Sub(tc.setInflation)
require.True(t, diffInflation.Equal(tc.expChange),
"Test Index: %v\nDiff: %v\nExpected: %v\n", i, diffInflation, tc.expChange)
}
}
func TestBlockProvision(t *testing.T) {
minter := InitialMinter(sdk.NewDecWithPrec(1, 1))
params := DefaultParams()
secondsPerYear := int64(60 * 60 * 8766)
tests := []struct {
annualProvisions int64
expProvisions int64
}{
{secondsPerYear / 5, 1},
{secondsPerYear/5 + 1, 1},
{(secondsPerYear / 5) * 2, 2},
{(secondsPerYear / 5) / 2, 0},
}
for i, tc := range tests {
minter.AnnualProvisions = sdk.NewDec(tc.annualProvisions)
provisions := minter.BlockProvision(params)
expProvisions := sdk.NewCoin(params.MintDenom,
sdk.NewInt(tc.expProvisions))
require.True(t, expProvisions.IsEqual(provisions),
"test: %v\n\tExp: %v\n\tGot: %v\n",
i, tc.expProvisions, provisions)
}
}
// Benchmarking :)
// previously using sdk.Int operations:
// BenchmarkBlockProvision-4 5000000 220 ns/op
//
// using sdk.Dec operations: (current implementation)
// BenchmarkBlockProvision-4 3000000 429 ns/op
func BenchmarkBlockProvision(b *testing.B) {
minter := InitialMinter(sdk.NewDecWithPrec(1, 1))
params := DefaultParams()
s1 := rand.NewSource(100)
r1 := rand.New(s1)
minter.AnnualProvisions = sdk.NewDec(r1.Int63n(1000000))
for n := 0; n < b.N; n++ {
minter.BlockProvision(params)
}
}

View File

@ -2,6 +2,7 @@ package mint
import (
"fmt"
stakeTypes "github.com/cosmos/cosmos-sdk/x/stake/types"
sdk "github.com/cosmos/cosmos-sdk/types"
@ -14,6 +15,20 @@ type Params struct {
InflationMax sdk.Dec `json:"inflation_max"` // maximum inflation rate
InflationMin sdk.Dec `json:"inflation_min"` // minimum inflation rate
GoalBonded sdk.Dec `json:"goal_bonded"` // goal of percent bonded atoms
BlocksPerYear uint64 `json:"blocks_per_year"` // expected blocks per year
}
func NewParams(mintDenom string, inflationRateChange, inflationMax,
inflationMin, goalBonded sdk.Dec, blocksPerYear uint64) Params {
return Params{
MintDenom: mintDenom,
InflationRateChange: inflationRateChange,
InflationMax: inflationMax,
InflationMin: inflationMin,
GoalBonded: goalBonded,
BlocksPerYear: blocksPerYear,
}
}
// default minting module parameters
@ -24,6 +39,7 @@ func DefaultParams() Params {
InflationMax: sdk.NewDecWithPrec(20, 2),
InflationMin: sdk.NewDecWithPrec(7, 2),
GoalBonded: sdk.NewDecWithPrec(67, 2),
BlocksPerYear: uint64(60 * 60 * 8766 / 5), // assuming 5 second block times
}
}

View File

@ -600,6 +600,9 @@ func (k Keeper) BeginRedelegation(ctx sdk.Context, delAddr sdk.AccAddress,
}
rounded := returnAmount.TruncateInt()
if rounded.IsZero() {
return types.Redelegation{}, types.ErrVerySmallRedelegation(k.Codespace())
}
returnCoin := sdk.NewCoin(k.BondDenom(ctx), rounded)
change := returnAmount.Sub(sdk.NewDecFromInt(rounded))
@ -612,6 +615,7 @@ func (k Keeper) BeginRedelegation(ctx sdk.Context, delAddr sdk.AccAddress,
if !found {
return types.Redelegation{}, types.ErrBadRedelegationDst(k.Codespace())
}
sharesCreated, err := k.Delegate(ctx, delAddr, returnCoin, dstValidator, false)
if err != nil {
return types.Redelegation{}, err

View File

@ -10,7 +10,7 @@ import (
// Implements ValidatorSet
var _ sdk.ValidatorSet = Keeper{}
// iterate through the active validator set and perform the provided function
// iterate through the validator set and perform the provided function
func (k Keeper) IterateValidators(ctx sdk.Context, fn func(index int64, validator sdk.Validator) (stop bool)) {
store := ctx.KVStore(k.storeKey)
iterator := sdk.KVStorePrefixIterator(store, ValidatorsKey)
@ -27,7 +27,7 @@ func (k Keeper) IterateValidators(ctx sdk.Context, fn func(index int64, validato
iterator.Close()
}
// iterate through the active validator set and perform the provided function
// iterate through the bonded validator set and perform the provided function
func (k Keeper) IterateBondedValidatorsByPower(ctx sdk.Context, fn func(index int64, validator sdk.Validator) (stop bool)) {
store := ctx.KVStore(k.storeKey)
maxValidators := k.MaxValidators(ctx)

View File

@ -33,6 +33,16 @@ func AllInvariants(ck bank.Keeper, k stake.Keeper,
return err
}
err = PositiveDelegationInvariant(k)(app)
if err != nil {
return err
}
err = DelegatorSharesInvariant(k)(app)
if err != nil {
return err
}
err = ValidatorSetInvariant(k)(app)
return err
}
@ -131,6 +141,53 @@ func PositivePowerInvariant(k stake.Keeper) simulation.Invariant {
}
}
// PositiveDelegationInvariant checks that all stored delegations have > 0 shares.
func PositiveDelegationInvariant(k stake.Keeper) simulation.Invariant {
return func(app *baseapp.BaseApp) error {
ctx := app.NewContext(false, abci.Header{})
delegations := k.GetAllDelegations(ctx)
for _, delegation := range delegations {
if delegation.Shares.IsNegative() {
return fmt.Errorf("delegation with negative shares: %+v", delegation)
}
if delegation.Shares.IsZero() {
return fmt.Errorf("delegation with zero shares: %+v", delegation)
}
}
return nil
}
}
// DelegatorSharesInvariant checks whether all the delegator shares which persist
// in the delegator object add up to the correct total delegator shares
// amount stored in each validator
func DelegatorSharesInvariant(k stake.Keeper) simulation.Invariant {
return func(app *baseapp.BaseApp) error {
ctx := app.NewContext(false, abci.Header{})
validators := k.GetAllValidators(ctx)
for _, validator := range validators {
valTotalDelShares := validator.GetDelegatorShares()
totalDelShares := sdk.ZeroDec()
delegations := k.GetValidatorDelegations(ctx, validator.GetOperator())
for _, delegation := range delegations {
totalDelShares = totalDelShares.Add(delegation.Shares)
}
if !valTotalDelShares.Equal(totalDelShares) {
return fmt.Errorf("broken delegator shares invariance:\n"+
"\tvalidator.DelegatorShares: %v\n"+
"\tsum of Delegator.Shares: %v", valTotalDelShares, totalDelShares)
}
}
return nil
}
}
// ValidatorSetInvariant checks equivalence of Tendermint validator set and SDK validator set
func ValidatorSetInvariant(k stake.Keeper) simulation.Invariant {
return func(app *baseapp.BaseApp) error {

View File

@ -159,6 +159,10 @@ func ErrSelfRedelegation(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidDelegation, "cannot redelegate to the same validator")
}
func ErrVerySmallRedelegation(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidDelegation, "too few tokens to redelegate, truncates to zero tokens")
}
func ErrBadRedelegationDst(codespace sdk.CodespaceType) sdk.Error {
return sdk.NewError(codespace, CodeInvalidDelegation, "redelegation validator not found")
}