Inflation bug fixes (#2982)

* PENDING.md; swap BeginBlocker ordering
* Calculate inflation every block
* Update x/mint spec
* Reset distribution info bond height instead
This commit is contained in:
Christopher Goes 2018-12-04 19:17:02 +01:00 committed by Jack Zampolin
parent bcfd93f544
commit dfd00a661a
11 changed files with 93 additions and 72 deletions

View File

@ -58,5 +58,6 @@ BUG FIXES
* Gaia * Gaia
* SDK * SDK
* \#2967 Change ordering of `mint.BeginBlocker` and `distr.BeginBlocker`, recalculate inflation each block
* Tendermint * Tendermint

View File

@ -189,12 +189,12 @@ func MakeCodec() *codec.Codec {
// application updates every end block // application updates every end block
func (app *GaiaApp) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { func (app *GaiaApp) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock {
// distribute rewards from previous block // mint new tokens for the previous block
distr.BeginBlocker(ctx, req, app.distrKeeper)
// mint new tokens for this new block
mint.BeginBlocker(ctx, app.mintKeeper) mint.BeginBlocker(ctx, app.mintKeeper)
// distribute rewards for the previous block
distr.BeginBlocker(ctx, req, app.distrKeeper)
// slash anyone who double signed. // slash anyone who double signed.
// NOTE: This should happen after distr.BeginBlocker so that // NOTE: This should happen after distr.BeginBlocker so that
// there is nothing left over in the validator fee pool, // there is nothing left over in the validator fee pool,

View File

@ -56,34 +56,13 @@ func (app *GaiaApp) ExportAppStateAndValidators(forZeroHeight bool) (
// prepare for fresh start at zero height // prepare for fresh start at zero height
func (app *GaiaApp) prepForZeroHeightGenesis(ctx sdk.Context) { func (app *GaiaApp) prepForZeroHeightGenesis(ctx sdk.Context) {
/* TODO XXX check some invariants */ /* Just to be safe, assert the invariants on current state. */
app.assertRuntimeInvariantsOnContext(ctx)
height := ctx.BlockHeight()
valAccum := sdk.ZeroDec()
vdiIter := func(_ int64, vdi distr.ValidatorDistInfo) bool {
lastValPower := app.stakeKeeper.GetLastValidatorPower(ctx, vdi.OperatorAddr)
valAccum = valAccum.Add(vdi.GetValAccum(height, sdk.NewDecFromInt(lastValPower)))
return false
}
app.distrKeeper.IterateValidatorDistInfos(ctx, vdiIter)
lastTotalPower := sdk.NewDecFromInt(app.stakeKeeper.GetLastTotalPower(ctx))
totalAccum := app.distrKeeper.GetFeePool(ctx).GetTotalValAccum(height, lastTotalPower)
if !totalAccum.Equal(valAccum) {
panic(fmt.Errorf("validator accum invariance: \n\tfee pool totalAccum: %v"+
"\n\tvalidator accum \t%v\n", totalAccum.String(), valAccum.String()))
}
fmt.Printf("accum invariant ok!\n")
/* END TODO XXX */
/* Handle fee distribution state. */ /* Handle fee distribution state. */
// withdraw all delegator & validator rewards // withdraw all delegator & validator rewards
vdiIter = func(_ int64, valInfo distr.ValidatorDistInfo) (stop bool) { vdiIter := func(_ int64, valInfo distr.ValidatorDistInfo) (stop bool) {
err := app.distrKeeper.WithdrawValidatorRewardsAll(ctx, valInfo.OperatorAddr) err := app.distrKeeper.WithdrawValidatorRewardsAll(ctx, valInfo.OperatorAddr)
if err != nil { if err != nil {
panic(err) panic(err)
@ -102,10 +81,19 @@ func (app *GaiaApp) prepForZeroHeightGenesis(ctx sdk.Context) {
} }
app.distrKeeper.IterateDelegationDistInfos(ctx, ddiIter) app.distrKeeper.IterateDelegationDistInfos(ctx, ddiIter)
// delete all distribution infos app.assertRuntimeInvariantsOnContext(ctx)
// these will be recreated in InitGenesis
app.distrKeeper.RemoveValidatorDistInfos(ctx) // set distribution info withdrawal heights to 0
app.distrKeeper.RemoveDelegationDistInfos(ctx) app.distrKeeper.IterateDelegationDistInfos(ctx, func(_ int64, delInfo distr.DelegationDistInfo) (stop bool) {
delInfo.DelPoolWithdrawalHeight = 0
app.distrKeeper.SetDelegationDistInfo(ctx, delInfo)
return false
})
app.distrKeeper.IterateValidatorDistInfos(ctx, func(_ int64, valInfo distr.ValidatorDistInfo) (stop bool) {
valInfo.FeePoolWithdrawalHeight = 0
app.distrKeeper.SetValidatorDistInfo(ctx, valInfo)
return false
})
// assert that the fee pool is empty // assert that the fee pool is empty
feePool := app.distrKeeper.GetFeePool(ctx) feePool := app.distrKeeper.GetFeePool(ctx)
@ -119,7 +107,7 @@ func (app *GaiaApp) prepForZeroHeightGenesis(ctx sdk.Context) {
} }
// reset fee pool height, save fee pool // reset fee pool height, save fee pool
feePool.TotalValAccum.UpdateHeight = 0 feePool.TotalValAccum = distr.NewTotalAccum(0)
app.distrKeeper.SetFeePool(ctx, feePool) app.distrKeeper.SetFeePool(ctx, feePool)
/* Handle stake state. */ /* Handle stake state. */

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"time" "time"
sdk "github.com/cosmos/cosmos-sdk/types"
banksim "github.com/cosmos/cosmos-sdk/x/bank/simulation" banksim "github.com/cosmos/cosmos-sdk/x/bank/simulation"
distrsim "github.com/cosmos/cosmos-sdk/x/distribution/simulation" distrsim "github.com/cosmos/cosmos-sdk/x/distribution/simulation"
"github.com/cosmos/cosmos-sdk/x/mock/simulation" "github.com/cosmos/cosmos-sdk/x/mock/simulation"
@ -22,9 +23,13 @@ func (app *GaiaApp) runtimeInvariants() []simulation.Invariant {
} }
func (app *GaiaApp) assertRuntimeInvariants() { func (app *GaiaApp) assertRuntimeInvariants() {
invariants := app.runtimeInvariants()
start := time.Now()
ctx := app.NewContext(false, abci.Header{Height: app.LastBlockHeight() + 1}) ctx := app.NewContext(false, abci.Header{Height: app.LastBlockHeight() + 1})
app.assertRuntimeInvariantsOnContext(ctx)
}
func (app *GaiaApp) assertRuntimeInvariantsOnContext(ctx sdk.Context) {
start := time.Now()
invariants := app.runtimeInvariants()
for _, inv := range invariants { for _, inv := range invariants {
if err := inv(ctx); err != nil { if err := inv(ctx); err != nil {
panic(fmt.Errorf("invariant broken: %s", err)) panic(fmt.Errorf("invariant broken: %s", err))

View File

@ -1,12 +1,12 @@
# Begin-Block # Begin-Block
Inflation occurs at the beginning of each block, however minting parameters Minting parameters are recalculated and inflation
are only calculated once per hour. paid at the beginning of each block.
## NextInflationRate ## NextInflationRate
The target annual inflation rate is recalculated at the first block of each new The target annual inflation rate is recalculated each block.
hour. The inflation is also subject to a rate change (positive or negative) 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 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 possible is defined to be 13% per year, however the annual inflation is capped
as between 7% and 20%. as between 7% and 20%.
@ -14,7 +14,7 @@ as between 7% and 20%.
``` ```
NextInflationRate(params Params, bondedRatio sdk.Dec) (inflation sdk.Dec) { NextInflationRate(params Params, bondedRatio sdk.Dec) (inflation sdk.Dec) {
inflationRateChangePerYear = (1 - bondedRatio/params.GoalBonded) * params.InflationRateChange inflationRateChangePerYear = (1 - bondedRatio/params.GoalBonded) * params.InflationRateChange
inflationRateChange = inflationRateChangePerYear/hrsPerYr inflationRateChange = inflationRateChangePerYear/blocksPerYr
// increase the new annual inflation for this next cycle // increase the new annual inflation for this next cycle
inflation += inflationRateChange inflation += inflationRateChange

View File

@ -8,7 +8,6 @@ The minter is a space for holding current inflation information.
```golang ```golang
type Minter struct { type Minter struct {
LastUpdate time.Time // time which the last update was made to the minter
Inflation sdk.Dec // current annual inflation rate Inflation sdk.Dec // current annual inflation rate
AnnualProvisions sdk.Dec // current annual exptected provisions AnnualProvisions sdk.Dec // current annual exptected provisions
} }
@ -30,4 +29,3 @@ type Params struct {
BlocksPerYear uint64 // expected blocks per year BlocksPerYear uint64 // expected blocks per year
} }
``` ```

View File

@ -60,6 +60,8 @@ var (
NewMsgWithdrawValidatorRewardsAll = types.NewMsgWithdrawValidatorRewardsAll NewMsgWithdrawValidatorRewardsAll = types.NewMsgWithdrawValidatorRewardsAll
NewDecCoins = types.NewDecCoins NewDecCoins = types.NewDecCoins
NewTotalAccum = types.NewTotalAccum
) )
const ( const (

View File

@ -193,3 +193,13 @@ func (coins DecCoins) HasNegative() bool {
} }
return false return false
} }
// return whether all coins are zero
func (coins DecCoins) IsZero() bool {
for _, coin := range coins {
if !coin.Amount.IsZero() {
return false
}
}
return true
}

View File

@ -1,31 +1,26 @@
package mint package mint
import ( import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
) )
// Inflate every block, update inflation parameters once per hour // Inflate every block, update inflation parameters once per hour
func BeginBlocker(ctx sdk.Context, k Keeper) { func BeginBlocker(ctx sdk.Context, k Keeper) {
blockTime := ctx.BlockHeader().Time // fetch stored minter & params
minter := k.GetMinter(ctx) minter := k.GetMinter(ctx)
params := k.GetParams(ctx) params := k.GetParams(ctx)
mintedCoin := minter.BlockProvision(params) // recalculate inflation rate
k.fck.AddCollectedFees(ctx, sdk.Coins{mintedCoin})
k.sk.InflateSupply(ctx, sdk.NewDecFromInt(mintedCoin.Amount))
if blockTime.Sub(minter.LastUpdate) < time.Hour {
return
}
// adjust the inflation, hourly-provision rate every hour
totalSupply := k.sk.TotalPower(ctx) totalSupply := k.sk.TotalPower(ctx)
bondedRatio := k.sk.BondedRatio(ctx) bondedRatio := k.sk.BondedRatio(ctx)
minter.Inflation = minter.NextInflationRate(params, bondedRatio) minter.Inflation = minter.NextInflationRate(params, bondedRatio)
minter.AnnualProvisions = minter.NextAnnualProvisions(params, totalSupply) minter.AnnualProvisions = minter.NextAnnualProvisions(params, totalSupply)
minter.LastUpdate = blockTime
k.SetMinter(ctx, minter) k.SetMinter(ctx, minter)
// mint coins, add to collected fees, update supply
mintedCoin := minter.BlockProvision(params)
k.fck.AddCollectedFees(ctx, sdk.Coins{mintedCoin})
k.sk.InflateSupply(ctx, sdk.NewDecFromInt(mintedCoin.Amount))
} }

View File

@ -2,24 +2,20 @@ package mint
import ( import (
"fmt" "fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
) )
// Minter represents the minting state // Minter represents the minting state
type Minter struct { type Minter struct {
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
Inflation sdk.Dec `json:"inflation"` // current annual inflation rate AnnualProvisions sdk.Dec `json:"annual_provisions"` // current annual expected provisions
AnnualProvisions sdk.Dec `json:"annual_provisions"` // current annual expected provisions
} }
// Create a new minter object // Create a new minter object
func NewMinter(lastUpdate time.Time, inflation, func NewMinter(inflation, annualProvisions sdk.Dec) Minter {
annualProvisions sdk.Dec) Minter {
return Minter{ return Minter{
LastUpdate: lastUpdate,
Inflation: inflation, Inflation: inflation,
AnnualProvisions: annualProvisions, AnnualProvisions: annualProvisions,
} }
@ -28,7 +24,6 @@ func NewMinter(lastUpdate time.Time, inflation,
// minter object for a new chain // minter object for a new chain
func InitialMinter(inflation sdk.Dec) Minter { func InitialMinter(inflation sdk.Dec) Minter {
return NewMinter( return NewMinter(
time.Unix(0, 0),
inflation, inflation,
sdk.NewDec(0), sdk.NewDec(0),
) )
@ -50,8 +45,6 @@ func validateMinter(minter Minter) error {
return nil return nil
} }
var hrsPerYr = sdk.NewDec(8766) // as defined by a julian year of 365.25 days
// get the new inflation rate for the next hour // get the new inflation rate for the next hour
func (m Minter) NextInflationRate(params Params, bondedRatio sdk.Dec) ( func (m Minter) NextInflationRate(params Params, bondedRatio sdk.Dec) (
inflation sdk.Dec) { inflation sdk.Dec) {
@ -66,7 +59,7 @@ func (m Minter) NextInflationRate(params Params, bondedRatio sdk.Dec) (
inflationRateChangePerYear := sdk.OneDec(). inflationRateChangePerYear := sdk.OneDec().
Sub(bondedRatio.Quo(params.GoalBonded)). Sub(bondedRatio.Quo(params.GoalBonded)).
Mul(params.InflationRateChange) Mul(params.InflationRateChange)
inflationRateChange := inflationRateChangePerYear.Quo(hrsPerYr) inflationRateChange := inflationRateChangePerYear.Quo(sdk.NewDec(int64(params.BlocksPerYear)))
// increase the new annual inflation for this next cycle // increase the new annual inflation for this next cycle
inflation = m.Inflation.Add(inflationRateChange) inflation = m.Inflation.Add(inflationRateChange)

View File

@ -12,6 +12,7 @@ import (
func TestNextInflation(t *testing.T) { func TestNextInflation(t *testing.T) {
minter := DefaultInitialMinter() minter := DefaultInitialMinter()
params := DefaultParams() params := DefaultParams()
blocksPerYr := sdk.NewDec(int64(params.BlocksPerYear))
// Governing Mechanism: // Governing Mechanism:
// inflationRateChangePerYear = (1- BondedRatio/ GoalBonded) * MaxInflationRateChange // inflationRateChangePerYear = (1- BondedRatio/ GoalBonded) * MaxInflationRateChange
@ -20,24 +21,24 @@ func TestNextInflation(t *testing.T) {
bondedRatio, setInflation, expChange sdk.Dec bondedRatio, setInflation, expChange sdk.Dec
}{ }{
// with 0% bonded atom supply the inflation should increase by InflationRateChange // with 0% bonded atom supply the inflation should increase by InflationRateChange
{sdk.ZeroDec(), sdk.NewDecWithPrec(7, 2), params.InflationRateChange.Quo(hrsPerYr)}, {sdk.ZeroDec(), sdk.NewDecWithPrec(7, 2), params.InflationRateChange.Quo(blocksPerYr)},
// 100% bonded, starting at 20% inflation and being reduced // 100% bonded, starting at 20% inflation and being reduced
// (1 - (1/0.67))*(0.13/8667) // (1 - (1/0.67))*(0.13/8667)
{sdk.OneDec(), sdk.NewDecWithPrec(20, 2), {sdk.OneDec(), sdk.NewDecWithPrec(20, 2),
sdk.OneDec().Sub(sdk.OneDec().Quo(params.GoalBonded)).Mul(params.InflationRateChange).Quo(hrsPerYr)}, sdk.OneDec().Sub(sdk.OneDec().Quo(params.GoalBonded)).Mul(params.InflationRateChange).Quo(blocksPerYr)},
// 50% bonded, starting at 10% inflation and being increased // 50% bonded, starting at 10% inflation and being increased
{sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(10, 2), {sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(10, 2),
sdk.OneDec().Sub(sdk.NewDecWithPrec(5, 1).Quo(params.GoalBonded)).Mul(params.InflationRateChange).Quo(hrsPerYr)}, sdk.OneDec().Sub(sdk.NewDecWithPrec(5, 1).Quo(params.GoalBonded)).Mul(params.InflationRateChange).Quo(blocksPerYr)},
// test 7% minimum stop (testing with 100% bonded) // test 7% minimum stop (testing with 100% bonded)
{sdk.OneDec(), sdk.NewDecWithPrec(7, 2), sdk.ZeroDec()}, {sdk.OneDec(), sdk.NewDecWithPrec(7, 2), sdk.ZeroDec()},
{sdk.OneDec(), sdk.NewDecWithPrec(70001, 6), sdk.NewDecWithPrec(-1, 6)}, {sdk.OneDec(), sdk.NewDecWithPrec(700000001, 10), sdk.NewDecWithPrec(-1, 10)},
// test 20% maximum stop (testing with 0% bonded) // test 20% maximum stop (testing with 0% bonded)
{sdk.ZeroDec(), sdk.NewDecWithPrec(20, 2), sdk.ZeroDec()}, {sdk.ZeroDec(), sdk.NewDecWithPrec(20, 2), sdk.ZeroDec()},
{sdk.ZeroDec(), sdk.NewDecWithPrec(199999, 6), sdk.NewDecWithPrec(1, 6)}, {sdk.ZeroDec(), sdk.NewDecWithPrec(1999999999, 10), sdk.NewDecWithPrec(1, 10)},
// perfect balance shouldn't change inflation // perfect balance shouldn't change inflation
{sdk.NewDecWithPrec(67, 2), sdk.NewDecWithPrec(15, 2), sdk.ZeroDec()}, {sdk.NewDecWithPrec(67, 2), sdk.NewDecWithPrec(15, 2), sdk.ZeroDec()},
@ -95,8 +96,36 @@ func BenchmarkBlockProvision(b *testing.B) {
r1 := rand.New(s1) r1 := rand.New(s1)
minter.AnnualProvisions = sdk.NewDec(r1.Int63n(1000000)) minter.AnnualProvisions = sdk.NewDec(r1.Int63n(1000000))
// run the Fib function b.N times // run the BlockProvision function b.N times
for n := 0; n < b.N; n++ { for n := 0; n < b.N; n++ {
minter.BlockProvision(params) minter.BlockProvision(params)
} }
} }
// Next inflation benchmarking
// BenchmarkNextInflation-4 1000000 1828 ns/op
func BenchmarkNextInflation(b *testing.B) {
minter := InitialMinter(sdk.NewDecWithPrec(1, 1))
params := DefaultParams()
bondedRatio := sdk.NewDecWithPrec(1, 1)
// run the NextInflationRate function b.N times
for n := 0; n < b.N; n++ {
minter.NextInflationRate(params, bondedRatio)
}
}
// Next annual provisions benchmarking
// BenchmarkNextAnnualProvisions-4 5000000 251 ns/op
func BenchmarkNextAnnualProvisions(b *testing.B) {
minter := InitialMinter(sdk.NewDecWithPrec(1, 1))
params := DefaultParams()
totalSupply := sdk.NewDec(100000000000000)
// run the NextAnnualProvisions function b.N times
for n := 0; n < b.N; n++ {
minter.NextAnnualProvisions(params, totalSupply)
}
}