diff --git a/PENDING.md b/PENDING.md index 3a92041c333a..804b1b318823 100644 --- a/PENDING.md +++ b/PENDING.md @@ -45,6 +45,7 @@ BREAKING CHANGES * [simulation] \#2162 Added back correct supply invariants * [x/slashing] \#2430 Simulate more slashes, check if validator is jailed before jailing * [x/stake] \#2393 Removed `CompleteUnbonding` and `CompleteRedelegation` Msg types, and instead added unbonding/redelegation queues to endblocker + * [x/mock/simulation] \#2501 Simulate transactions & invariants for fee distribution * [x/stake] \#1673 Validators are no longer deleted until they can no longer possibly be slashed * [\#1890](https://github.com/cosmos/cosmos-sdk/issues/1890) Start chain with initial state + sequence of transactions * [cli] Rename `gaiad init gentx` to `gaiad gentx`. diff --git a/cmd/gaia/app/app.go b/cmd/gaia/app/app.go index 8fba41f60072..24b56d406ba3 100644 --- a/cmd/gaia/app/app.go +++ b/cmd/gaia/app/app.go @@ -3,6 +3,10 @@ package app import ( "encoding/json" "fmt" + "io" + "os" + "sort" + bam "github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" @@ -19,9 +23,6 @@ import ( dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" tmtypes "github.com/tendermint/tendermint/types" - "io" - "os" - "sort" ) const ( @@ -334,8 +335,8 @@ var _ sdk.StakingHooks = Hooks{} func (h Hooks) OnValidatorCreated(ctx sdk.Context, addr sdk.ValAddress) { h.dh.OnValidatorCreated(ctx, addr) } -func (h Hooks) OnValidatorCommissionChange(ctx sdk.Context, addr sdk.ValAddress) { - h.dh.OnValidatorCommissionChange(ctx, addr) +func (h Hooks) OnValidatorModified(ctx sdk.Context, addr sdk.ValAddress) { + h.dh.OnValidatorModified(ctx, addr) } func (h Hooks) OnValidatorRemoved(ctx sdk.Context, addr sdk.ValAddress) { h.dh.OnValidatorRemoved(ctx, addr) @@ -343,8 +344,9 @@ func (h Hooks) OnValidatorRemoved(ctx sdk.Context, addr sdk.ValAddress) { func (h Hooks) OnValidatorBonded(ctx sdk.Context, addr sdk.ConsAddress) { h.sh.OnValidatorBonded(ctx, addr) } -func (h Hooks) OnValidatorBeginUnbonding(ctx sdk.Context, addr sdk.ConsAddress) { - h.sh.OnValidatorBeginUnbonding(ctx, addr) +func (h Hooks) OnValidatorBeginUnbonding(ctx sdk.Context, addr sdk.ConsAddress, operator sdk.ValAddress) { + h.dh.OnValidatorBeginUnbonding(ctx, addr, operator) + h.sh.OnValidatorBeginUnbonding(ctx, addr, operator) } func (h Hooks) OnDelegationCreated(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) { h.dh.OnDelegationCreated(ctx, delAddr, valAddr) diff --git a/cmd/gaia/app/sim_test.go b/cmd/gaia/app/sim_test.go index e24919e102e3..630165cd0157 100644 --- a/cmd/gaia/app/sim_test.go +++ b/cmd/gaia/app/sim_test.go @@ -14,8 +14,10 @@ import ( "github.com/tendermint/tendermint/libs/log" sdk "github.com/cosmos/cosmos-sdk/types" + authsim "github.com/cosmos/cosmos-sdk/x/auth/simulation" banksim "github.com/cosmos/cosmos-sdk/x/bank/simulation" distr "github.com/cosmos/cosmos-sdk/x/distribution" + distributionsim "github.com/cosmos/cosmos-sdk/x/distribution/simulation" "github.com/cosmos/cosmos-sdk/x/gov" govsim "github.com/cosmos/cosmos-sdk/x/gov/simulation" "github.com/cosmos/cosmos-sdk/x/mint" @@ -49,7 +51,7 @@ func appStateFn(r *rand.Rand, accs []simulation.Account) json.RawMessage { // Randomly generate some genesis accounts for _, acc := range accs { - coins := sdk.Coins{sdk.Coin{"steak", sdk.NewInt(100)}} + coins := sdk.Coins{sdk.Coin{"steak", sdk.NewInt(10000)}} genesisAccounts = append(genesisAccounts, GenesisAccount{ Address: acc.Address, Coins: coins, @@ -71,20 +73,16 @@ func appStateFn(r *rand.Rand, accs []simulation.Account) json.RawMessage { valAddrs[i] = valAddr validator := stake.NewValidator(valAddr, accs[i].PubKey, stake.Description{}) - validator.Tokens = sdk.NewDec(100) - validator.DelegatorShares = sdk.NewDec(100) - delegation := stake.Delegation{accs[i].Address, valAddr, sdk.NewDec(100), 0} + validator.Tokens = sdk.NewDec(10000) + validator.DelegatorShares = sdk.NewDec(10000) + delegation := stake.Delegation{accs[i].Address, valAddr, sdk.NewDec(10000), 0} validators = append(validators, validator) delegations = append(delegations, delegation) } - stakeGenesis.Pool.LooseTokens = sdk.NewDec(int64(100*250) + (numInitiallyBonded * 100)) + stakeGenesis.Pool.LooseTokens = sdk.NewDec(int64(10000*250) + (numInitiallyBonded * 10000)) stakeGenesis.Validators = validators stakeGenesis.Bonds = delegations - - // No inflation, for now mintGenesis := mint.DefaultGenesisState() - mintGenesis.Params.InflationMax = sdk.NewDec(0) - mintGenesis.Params.InflationMin = sdk.NewDec(0) genesis := GenesisState{ Accounts: genesisAccounts, @@ -106,7 +104,12 @@ func appStateFn(r *rand.Rand, accs []simulation.Account) json.RawMessage { func testAndRunTxs(app *GaiaApp) []simulation.WeightedOperation { return []simulation.WeightedOperation{ + {5, authsim.SimulateDeductFee(app.accountMapper, app.feeCollectionKeeper)}, {100, banksim.SingleInputSendMsg(app.accountMapper, app.bankKeeper)}, + {50, distributionsim.SimulateMsgSetWithdrawAddress(app.accountMapper, app.distrKeeper)}, + {50, distributionsim.SimulateMsgWithdrawDelegatorRewardsAll(app.accountMapper, app.distrKeeper)}, + {50, distributionsim.SimulateMsgWithdrawDelegatorReward(app.accountMapper, app.distrKeeper)}, + {50, distributionsim.SimulateMsgWithdrawValidatorRewardsAll(app.accountMapper, app.distrKeeper)}, {5, govsim.SimulateSubmittingVotingAndSlashingForProposal(app.govKeeper, app.stakeKeeper)}, {100, govsim.SimulateMsgDeposit(app.govKeeper, app.stakeKeeper)}, {100, stakesim.SimulateMsgCreateValidator(app.accountMapper, app.stakeKeeper)}, @@ -121,8 +124,9 @@ func testAndRunTxs(app *GaiaApp) []simulation.WeightedOperation { func invariants(app *GaiaApp) []simulation.Invariant { return []simulation.Invariant{ banksim.NonnegativeBalanceInvariant(app.accountMapper), + distributionsim.AllInvariants(app.bankKeeper, app.distrKeeper, app.accountMapper), govsim.AllInvariants(), - stakesim.AllInvariants(app.bankKeeper, app.stakeKeeper, app.accountMapper), + stakesim.AllInvariants(app.bankKeeper, app.stakeKeeper, app.feeCollectionKeeper, app.distrKeeper, app.accountMapper), slashingsim.AllInvariants(), } } diff --git a/docs/spec/staking/hooks.md b/docs/spec/staking/hooks.md index bcc496e3d6e0..7d24e32e6446 100644 --- a/docs/spec/staking/hooks.md +++ b/docs/spec/staking/hooks.md @@ -5,12 +5,12 @@ The staking module allow for the following hooks to be registered with staking e ``` golang // event hooks for staking validator object type StakingHooks interface { - OnValidatorCreated(ctx Context, address ValAddress) // called when a validator is created - OnValidatorCommissionChange(ctx Context, address ValAddress) // called when a validator's commission is modified - OnValidatorRemoved(ctx Context, address ValAddress) // called when a validator is deleted + OnValidatorCreated(ctx Context, address ValAddress) // Must be called when a validator is created + OnValidatorModified(ctx Context, address ValAddress) // Must be called when a validator's state changes + OnValidatorRemoved(ctx Context, address ValAddress) // Must be called when a validator is deleted OnValidatorBonded(ctx Context, address ConsAddress) // called when a validator is bonded - OnValidatorBeginUnbonding(ctx Context, address ConsAddress) // called when a validator begins unbonding + OnValidatorBeginUnbonding(ctx Context, address ConsAddress, operator ValAddress) // called when a validator begins unbonding OnDelegationCreated(ctx Context, delAddr AccAddress, valAddr ValAddress) // called when a delegation is created OnDelegationSharesModified(ctx Context, delAddr AccAddress, valAddr ValAddress) // called when a delegation's shares are modified diff --git a/types/decimal.go b/types/decimal.go index e9623995f008..05dd97795a48 100644 --- a/types/decimal.go +++ b/types/decimal.go @@ -247,6 +247,11 @@ func (d Dec) QuoInt(i Int) Dec { return Dec{mul} } +// is integer, e.g. decimals are zero. +func (d Dec) IsInteger() bool { + return new(big.Int).Rem(d.Int, precisionReuse).Sign() == 0 +} + func (d Dec) String() string { str := d.ToLeftPaddedWithDecimals(Precision) placement := len(str) - Precision diff --git a/types/stake.go b/types/stake.go index d529fa1794b7..1818919ab8e3 100644 --- a/types/stake.go +++ b/types/stake.go @@ -85,9 +85,9 @@ type ValidatorSet interface { // delegation bond for a delegated proof of stake system type Delegation interface { - GetDelegator() AccAddress // delegator AccAddress for the bond - GetValidator() ValAddress // validator operator address - GetShares() Dec // amount of validator's shares held in this delegation + GetDelegatorAddr() AccAddress // delegator AccAddress for the bond + GetValidatorAddr() ValAddress // validator operator address + GetShares() Dec // amount of validator's shares held in this delegation } // properties for the set of all delegations for a particular @@ -111,12 +111,12 @@ type DelegationSet interface { // event hooks for staking validator object type StakingHooks interface { - OnValidatorCreated(ctx Context, address ValAddress) // Must be called when a validator is created - OnValidatorCommissionChange(ctx Context, address ValAddress) // Must be called when a validator's commission is modified - OnValidatorRemoved(ctx Context, address ValAddress) // Must be called when a validator is deleted + OnValidatorCreated(ctx Context, address ValAddress) // Must be called when a validator is created + OnValidatorModified(ctx Context, address ValAddress) // Must be called when a validator's state changes + OnValidatorRemoved(ctx Context, address ValAddress) // Must be called when a validator is deleted - OnValidatorBonded(ctx Context, address ConsAddress) // Must be called when a validator is bonded - OnValidatorBeginUnbonding(ctx Context, address ConsAddress) // Must be called when a validator begins unbonding + OnValidatorBonded(ctx Context, address ConsAddress) // Must be called when a validator is bonded + OnValidatorBeginUnbonding(ctx Context, address ConsAddress, operator ValAddress) // Must be called when a validator begins unbonding OnDelegationCreated(ctx Context, delAddr AccAddress, valAddr ValAddress) // Must be called when a delegation is created OnDelegationSharesModified(ctx Context, delAddr AccAddress, valAddr ValAddress) // Must be called when a delegation's shares are modified diff --git a/x/auth/simulation/fake.go b/x/auth/simulation/fake.go new file mode 100644 index 000000000000..e89e70842268 --- /dev/null +++ b/x/auth/simulation/fake.go @@ -0,0 +1,62 @@ +package simulation + +import ( + "errors" + "fmt" + "math/big" + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" +) + +// SimulateDeductFee +func SimulateDeductFee(m auth.AccountMapper, f auth.FeeCollectionKeeper) simulation.Operation { + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, + accs []simulation.Account, event func(string)) ( + action string, fOp []simulation.FutureOperation, err error) { + + account := simulation.RandomAcc(r, accs) + stored := m.GetAccount(ctx, account.Address) + initCoins := stored.GetCoins() + + if len(initCoins) == 0 { + event(fmt.Sprintf("auth/SimulateDeductFee/false")) + return action, nil, nil + } + + denomIndex := r.Intn(len(initCoins)) + amt, err := randPositiveInt(r, initCoins[denomIndex].Amount) + if err != nil { + event(fmt.Sprintf("auth/SimulateDeductFee/false")) + return action, nil, nil + } + + coins := sdk.Coins{sdk.NewCoin(initCoins[denomIndex].Denom, amt)} + err = stored.SetCoins(initCoins.Minus(coins)) + if err != nil { + panic(err) + } + m.SetAccount(ctx, stored) + if !coins.IsNotNegative() { + panic("setting negative fees") + } + + f.AddCollectedFees(ctx, coins) + + event(fmt.Sprintf("auth/SimulateDeductFee/true")) + + action = "TestDeductFee" + return action, nil, nil + } +} + +func randPositiveInt(r *rand.Rand, max sdk.Int) (sdk.Int, error) { + if !max.GT(sdk.OneInt()) { + return sdk.Int{}, errors.New("max too small") + } + max = max.Sub(sdk.OneInt()) + return sdk.NewIntFromBigInt(new(big.Int).Rand(r, max.BigInt())).Add(sdk.OneInt()), nil +} diff --git a/x/distribution/alias.go b/x/distribution/alias.go index 7f14f82a45e4..5a7cbce4d53c 100644 --- a/x/distribution/alias.go +++ b/x/distribution/alias.go @@ -49,7 +49,7 @@ var ( NewMsgSetWithdrawAddress = types.NewMsgSetWithdrawAddress NewMsgWithdrawDelegatorRewardsAll = types.NewMsgWithdrawDelegatorRewardsAll - NewMsgWithdrawDelegationReward = types.NewMsgWithdrawDelegatorReward + NewMsgWithdrawDelegatorReward = types.NewMsgWithdrawDelegatorReward NewMsgWithdrawValidatorRewardsAll = types.NewMsgWithdrawValidatorRewardsAll ) diff --git a/x/distribution/handler.go b/x/distribution/handler.go index 661d7d32aa31..624589cead28 100644 --- a/x/distribution/handler.go +++ b/x/distribution/handler.go @@ -58,7 +58,10 @@ func handleMsgWithdrawDelegatorRewardsAll(ctx sdk.Context, msg types.MsgWithdraw func handleMsgWithdrawDelegatorReward(ctx sdk.Context, msg types.MsgWithdrawDelegatorReward, k keeper.Keeper) sdk.Result { - k.WithdrawDelegationReward(ctx, msg.DelegatorAddr, msg.ValidatorAddr) + err := k.WithdrawDelegationReward(ctx, msg.DelegatorAddr, msg.ValidatorAddr) + if err != nil { + return err.Result() + } tags := sdk.NewTags( tags.Action, tags.ActionWithdrawDelegatorReward, @@ -72,7 +75,10 @@ func handleMsgWithdrawDelegatorReward(ctx sdk.Context, msg types.MsgWithdrawDele func handleMsgWithdrawValidatorRewardsAll(ctx sdk.Context, msg types.MsgWithdrawValidatorRewardsAll, k keeper.Keeper) sdk.Result { - k.WithdrawValidatorRewardsAll(ctx, msg.ValidatorAddr) + err := k.WithdrawValidatorRewardsAll(ctx, msg.ValidatorAddr) + if err != nil { + return err.Result() + } tags := sdk.NewTags( tags.Action, tags.ActionWithdrawValidatorRewardsAll, diff --git a/x/distribution/keeper/delegation.go b/x/distribution/keeper/delegation.go index b7443a0c1151..784a75c7973a 100644 --- a/x/distribution/keeper/delegation.go +++ b/x/distribution/keeper/delegation.go @@ -5,6 +5,13 @@ import ( "github.com/cosmos/cosmos-sdk/x/distribution/types" ) +// check whether a delegator distribution info exists +func (k Keeper) HasDelegationDistInfo(ctx sdk.Context, delAddr sdk.AccAddress, + valOperatorAddr sdk.ValAddress) (has bool) { + store := ctx.KVStore(k.storeKey) + return store.Has(GetDelegationDistInfoKey(delAddr, valOperatorAddr)) +} + // get the delegator distribution info func (k Keeper) GetDelegationDistInfo(ctx sdk.Context, delAddr sdk.AccAddress, valOperatorAddr sdk.ValAddress) (ddi types.DelegationDistInfo) { @@ -62,29 +69,40 @@ func (k Keeper) RemoveDelegatorWithdrawAddr(ctx sdk.Context, delAddr, withdrawAd //___________________________________________________________________________________________ -// withdraw all the rewards for a single delegation +// Withdraw all the rewards for a single delegation. +// NOTE: This gets called "onDelegationSharesModified", +// meaning any changes to bonded coins. func (k Keeper) WithdrawDelegationReward(ctx sdk.Context, delegatorAddr sdk.AccAddress, - validatorAddr sdk.ValAddress) { + valAddr sdk.ValAddress) sdk.Error { + if !k.HasDelegationDistInfo(ctx, delegatorAddr, valAddr) { + return types.ErrNoDelegationDistInfo(k.codespace) + } + + // TODO: Reconcile with duplicate code in getDelegatorRewardsAll. height := ctx.BlockHeight() - bondedTokens := k.stakeKeeper.TotalPower(ctx) + lastTotalPower := k.stakeKeeper.GetLastTotalPower(ctx) + lastValPower := k.stakeKeeper.GetLastValidatorPower(ctx, valAddr) feePool := k.GetFeePool(ctx) - delInfo := k.GetDelegationDistInfo(ctx, delegatorAddr, validatorAddr) - valInfo := k.GetValidatorDistInfo(ctx, validatorAddr) - validator := k.stakeKeeper.Validator(ctx, validatorAddr) - delegation := k.stakeKeeper.Delegation(ctx, delegatorAddr, validatorAddr) + delInfo := k.GetDelegationDistInfo(ctx, delegatorAddr, valAddr) + valInfo := k.GetValidatorDistInfo(ctx, valAddr) + validator := k.stakeKeeper.Validator(ctx, valAddr) + delegation := k.stakeKeeper.Delegation(ctx, delegatorAddr, valAddr) - delInfo, valInfo, feePool, withdraw := delInfo.WithdrawRewards(feePool, valInfo, height, bondedTokens, - validator.GetTokens(), validator.GetDelegatorShares(), delegation.GetShares(), validator.GetCommission()) + delInfo, valInfo, feePool, withdraw := delInfo.WithdrawRewards(feePool, valInfo, height, lastTotalPower, + lastValPower, validator.GetDelegatorShares(), delegation.GetShares(), validator.GetCommission()) - k.SetFeePool(ctx, feePool) k.SetValidatorDistInfo(ctx, valInfo) k.SetDelegationDistInfo(ctx, delInfo) withdrawAddr := k.GetDelegatorWithdrawAddr(ctx, delegatorAddr) - _, _, err := k.bankKeeper.AddCoins(ctx, withdrawAddr, withdraw.TruncateDecimal()) + coinsToAdd, change := withdraw.TruncateDecimal() + feePool.CommunityPool = feePool.CommunityPool.Plus(change) + k.SetFeePool(ctx, feePool) + _, _, err := k.bankKeeper.AddCoins(ctx, withdrawAddr, coinsToAdd) if err != nil { panic(err) } + return nil } //___________________________________________________________________________________________ @@ -93,8 +111,12 @@ func (k Keeper) WithdrawDelegationReward(ctx sdk.Context, delegatorAddr sdk.AccA func (k Keeper) WithdrawDelegationRewardsAll(ctx sdk.Context, delegatorAddr sdk.AccAddress) { height := ctx.BlockHeight() withdraw := k.getDelegatorRewardsAll(ctx, delegatorAddr, height) + feePool := k.GetFeePool(ctx) withdrawAddr := k.GetDelegatorWithdrawAddr(ctx, delegatorAddr) - _, _, err := k.bankKeeper.AddCoins(ctx, withdrawAddr, withdraw.TruncateDecimal()) + coinsToAdd, change := withdraw.TruncateDecimal() + feePool.CommunityPool = feePool.CommunityPool.Plus(change) + k.SetFeePool(ctx, feePool) + _, _, err := k.bankKeeper.AddCoins(ctx, withdrawAddr, coinsToAdd) if err != nil { panic(err) } @@ -104,19 +126,21 @@ func (k Keeper) WithdrawDelegationRewardsAll(ctx sdk.Context, delegatorAddr sdk. func (k Keeper) getDelegatorRewardsAll(ctx sdk.Context, delAddr sdk.AccAddress, height int64) types.DecCoins { withdraw := types.DecCoins{} - bondedTokens := k.stakeKeeper.TotalPower(ctx) - feePool := k.GetFeePool(ctx) + lastTotalPower := k.stakeKeeper.GetLastTotalPower(ctx) // iterate over all the delegations + // TODO: Reconcile with duplicate code in WithdrawDelegationReward. operationAtDelegation := func(_ int64, del sdk.Delegation) (stop bool) { - valAddr := del.GetValidator() + feePool := k.GetFeePool(ctx) + valAddr := del.GetValidatorAddr() + lastValPower := k.stakeKeeper.GetLastValidatorPower(ctx, valAddr) delInfo := k.GetDelegationDistInfo(ctx, delAddr, valAddr) valInfo := k.GetValidatorDistInfo(ctx, valAddr) validator := k.stakeKeeper.Validator(ctx, valAddr) delegation := k.stakeKeeper.Delegation(ctx, delAddr, valAddr) - delInfo, valInfo, feePool, diWithdraw := delInfo.WithdrawRewards(feePool, valInfo, height, bondedTokens, - validator.GetTokens(), validator.GetDelegatorShares(), delegation.GetShares(), validator.GetCommission()) + delInfo, valInfo, feePool, diWithdraw := delInfo.WithdrawRewards(feePool, valInfo, height, lastTotalPower, + lastValPower, validator.GetDelegatorShares(), delegation.GetShares(), validator.GetCommission()) withdraw = withdraw.Plus(diWithdraw) k.SetFeePool(ctx, feePool) k.SetValidatorDistInfo(ctx, valInfo) @@ -124,7 +148,5 @@ func (k Keeper) getDelegatorRewardsAll(ctx sdk.Context, delAddr sdk.AccAddress, return false } k.stakeKeeper.IterateDelegations(ctx, delAddr, operationAtDelegation) - - k.SetFeePool(ctx, feePool) return withdraw } diff --git a/x/distribution/keeper/delegation_test.go b/x/distribution/keeper/delegation_test.go index 3455d48c813a..8060fcdf2d14 100644 --- a/x/distribution/keeper/delegation_test.go +++ b/x/distribution/keeper/delegation_test.go @@ -34,6 +34,8 @@ func TestWithdrawDelegationRewardBasic(t *testing.T) { // withdraw delegation ctx = ctx.WithBlockHeight(1) + sk.SetLastTotalPower(ctx, sdk.NewDec(10)) + sk.SetLastValidatorPower(ctx, valOpAddr1, sdk.NewDec(10)) keeper.WithdrawDelegationReward(ctx, delAddr1, valOpAddr1) amt = accMapper.GetAccount(ctx, delAddr1).GetCoins().AmountOf(denom) @@ -204,8 +206,6 @@ func TestWithdrawDelegationRewardsAll(t *testing.T) { got = stakeHandler(ctx, msgCreateValidator) require.True(t, got.IsOK(), "expected msg to be ok, got %v", got) - _ = sk.ApplyAndReturnValidatorSetUpdates(ctx) - // delegate to all the validators msgDelegate := stake.NewTestMsgDelegate(delAddr1, valOpAddr1, 10) require.True(t, stakeHandler(ctx, msgDelegate).IsOK()) @@ -214,6 +214,9 @@ func TestWithdrawDelegationRewardsAll(t *testing.T) { msgDelegate = stake.NewTestMsgDelegate(delAddr1, valOpAddr3, 30) require.True(t, stakeHandler(ctx, msgDelegate).IsOK()) + // Update sk's LastValidatorPower/LastTotalPowers. + _ = sk.ApplyAndReturnValidatorSetUpdates(ctx) + // 40 tokens left after delegating 60 of them amt := accMapper.GetAccount(ctx, delAddr1).GetCoins().AmountOf(denom) require.Equal(t, int64(40), amt.Int64()) diff --git a/x/distribution/keeper/hooks.go b/x/distribution/keeper/hooks.go index d551cc3a3e3a..aeb394f26e28 100644 --- a/x/distribution/keeper/hooks.go +++ b/x/distribution/keeper/hooks.go @@ -14,16 +14,18 @@ func (k Keeper) onValidatorCreated(ctx sdk.Context, addr sdk.ValAddress) { vdi := types.ValidatorDistInfo{ OperatorAddr: addr, FeePoolWithdrawalHeight: height, - Pool: types.DecCoins{}, - PoolCommission: types.DecCoins{}, - DelAccum: types.NewTotalAccum(height), + Pool: types.DecCoins{}, + PoolCommission: types.DecCoins{}, + DelAccum: types.NewTotalAccum(height), } k.SetValidatorDistInfo(ctx, vdi) } // Withdrawal all validator rewards -func (k Keeper) onValidatorCommissionChange(ctx sdk.Context, addr sdk.ValAddress) { - k.WithdrawValidatorRewardsAll(ctx, addr) +func (k Keeper) onValidatorModified(ctx sdk.Context, addr sdk.ValAddress) { + if err := k.WithdrawValidatorRewardsAll(ctx, addr); err != nil { + panic(err) + } } // Withdrawal all validator distribution rewards and cleanup the distribution record @@ -50,7 +52,9 @@ func (k Keeper) onDelegationCreated(ctx sdk.Context, delAddr sdk.AccAddress, func (k Keeper) onDelegationSharesModified(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) { - k.WithdrawDelegationReward(ctx, delAddr, valAddr) + if err := k.WithdrawDelegationReward(ctx, delAddr, valAddr); err != nil { + panic(err) + } } // Withdrawal all validator distribution rewards and cleanup the distribution record @@ -76,8 +80,8 @@ func (k Keeper) Hooks() Hooks { return Hooks{k} } func (h Hooks) OnValidatorCreated(ctx sdk.Context, addr sdk.ValAddress) { h.k.onValidatorCreated(ctx, addr) } -func (h Hooks) OnValidatorCommissionChange(ctx sdk.Context, addr sdk.ValAddress) { - h.k.onValidatorCommissionChange(ctx, addr) +func (h Hooks) OnValidatorModified(ctx sdk.Context, addr sdk.ValAddress) { + h.k.onValidatorModified(ctx, addr) } func (h Hooks) OnValidatorRemoved(ctx sdk.Context, addr sdk.ValAddress) { h.k.onValidatorRemoved(ctx, addr) @@ -91,7 +95,9 @@ func (h Hooks) OnDelegationSharesModified(ctx sdk.Context, delAddr sdk.AccAddres func (h Hooks) OnDelegationRemoved(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) { h.k.onDelegationRemoved(ctx, delAddr, valAddr) } +func (h Hooks) OnValidatorBeginUnbonding(ctx sdk.Context, _ sdk.ConsAddress, addr sdk.ValAddress) { + h.k.onValidatorModified(ctx, addr) +} // nolint - unused hooks for interface -func (h Hooks) OnValidatorBonded(ctx sdk.Context, addr sdk.ConsAddress) {} -func (h Hooks) OnValidatorBeginUnbonding(ctx sdk.Context, addr sdk.ConsAddress) {} +func (h Hooks) OnValidatorBonded(ctx sdk.Context, addr sdk.ConsAddress) {} diff --git a/x/distribution/keeper/keeper.go b/x/distribution/keeper/keeper.go index 0ccf76ca63db..3919a18aedf7 100644 --- a/x/distribution/keeper/keeper.go +++ b/x/distribution/keeper/keeper.go @@ -55,6 +55,17 @@ func (k Keeper) SetFeePool(ctx sdk.Context, feePool types.FeePool) { store.Set(FeePoolKey, b) } +// get the total validator accum for the ctx height +// in the fee pool +func (k Keeper) GetFeePoolValAccum(ctx sdk.Context) sdk.Dec { + + // withdraw self-delegation + height := ctx.BlockHeight() + totalPower := k.stakeKeeper.GetLastTotalPower(ctx) + fp := k.GetFeePool(ctx) + return fp.GetTotalValAccum(height, totalPower) +} + //______________________________________________________________________ // set the proposer public key for this block diff --git a/x/distribution/keeper/keeper_test.go b/x/distribution/keeper/keeper_test.go index 824430511387..f8eb0925d538 100644 --- a/x/distribution/keeper/keeper_test.go +++ b/x/distribution/keeper/keeper_test.go @@ -29,9 +29,9 @@ func TestSetGetFeePool(t *testing.T) { ctx, _, keeper, _, _ := CreateTestInputDefault(t, false, 0) fp := types.InitialFeePool() - fp.ValAccum.UpdateHeight = 777 + fp.TotalValAccum.UpdateHeight = 777 keeper.SetFeePool(ctx, fp) res := keeper.GetFeePool(ctx) - require.Equal(t, fp.ValAccum, res.ValAccum) + require.Equal(t, fp.TotalValAccum, res.TotalValAccum) } diff --git a/x/distribution/keeper/test_common.go b/x/distribution/keeper/test_common.go index 59b615ec8a1a..42f3861fbe5a 100644 --- a/x/distribution/keeper/test_common.go +++ b/x/distribution/keeper/test_common.go @@ -114,7 +114,6 @@ func CreateTestInputAdvanced(t *testing.T, isCheckTx bool, initCoins int64, sk := stake.NewKeeper(cdc, keyStake, tkeyStake, ck, pk.Subspace(stake.DefaultParamspace), stake.DefaultCodespace) sk.SetPool(ctx, stake.InitialPool()) sk.SetParams(ctx, stake.DefaultParams()) - sk.InitIntraTxCounter(ctx) // fill all the addresses with some coins, set the loose pool tokens simultaneously for _, addr := range addrs { diff --git a/x/distribution/keeper/validator.go b/x/distribution/keeper/validator.go index ae07d8b3ffff..1ad8bef7bd9a 100644 --- a/x/distribution/keeper/validator.go +++ b/x/distribution/keeper/validator.go @@ -5,6 +5,13 @@ import ( "github.com/cosmos/cosmos-sdk/x/distribution/types" ) +// check whether a validator has distribution info +func (k Keeper) HasValidatorDistInfo(ctx sdk.Context, + operatorAddr sdk.ValAddress) (exists bool) { + store := ctx.KVStore(k.storeKey) + return store.Has(GetValidatorDistInfoKey(operatorAddr)) +} + // get the validator distribution info func (k Keeper) GetValidatorDistInfo(ctx sdk.Context, operatorAddr sdk.ValAddress) (vdi types.ValidatorDistInfo) { @@ -33,28 +40,69 @@ func (k Keeper) RemoveValidatorDistInfo(ctx sdk.Context, valAddr sdk.ValAddress) store.Delete(GetValidatorDistInfoKey(valAddr)) } +// Get the calculated accum of a validator at the current block +// without affecting the state. +func (k Keeper) GetValidatorAccum(ctx sdk.Context, operatorAddr sdk.ValAddress) (sdk.Dec, sdk.Error) { + if !k.HasValidatorDistInfo(ctx, operatorAddr) { + return sdk.Dec{}, types.ErrNoValidatorDistInfo(k.codespace) + } + + // withdraw self-delegation + height := ctx.BlockHeight() + lastValPower := k.stakeKeeper.GetLastValidatorPower(ctx, operatorAddr) + valInfo := k.GetValidatorDistInfo(ctx, operatorAddr) + accum := valInfo.GetAccum(height, lastValPower) + + return accum, nil +} + // withdrawal all the validator rewards including the commission -func (k Keeper) WithdrawValidatorRewardsAll(ctx sdk.Context, operatorAddr sdk.ValAddress) { +func (k Keeper) WithdrawValidatorRewardsAll(ctx sdk.Context, operatorAddr sdk.ValAddress) sdk.Error { + + if !k.HasValidatorDistInfo(ctx, operatorAddr) { + return types.ErrNoValidatorDistInfo(k.codespace) + } // withdraw self-delegation height := ctx.BlockHeight() validator := k.stakeKeeper.Validator(ctx, operatorAddr) + lastValPower := k.stakeKeeper.GetLastValidatorPower(ctx, operatorAddr) accAddr := sdk.AccAddress(operatorAddr.Bytes()) withdraw := k.getDelegatorRewardsAll(ctx, accAddr, height) // withdrawal validator commission rewards - bondedTokens := k.stakeKeeper.TotalPower(ctx) + lastTotalPower := k.stakeKeeper.GetLastTotalPower(ctx) valInfo := k.GetValidatorDistInfo(ctx, operatorAddr) feePool := k.GetFeePool(ctx) - valInfo, feePool, commission := valInfo.WithdrawCommission(feePool, height, bondedTokens, - validator.GetTokens(), validator.GetCommission()) + valInfo, feePool, commission := valInfo.WithdrawCommission(feePool, height, lastTotalPower, + lastValPower, validator.GetCommission()) withdraw = withdraw.Plus(commission) k.SetValidatorDistInfo(ctx, valInfo) - k.SetFeePool(ctx, feePool) withdrawAddr := k.GetDelegatorWithdrawAddr(ctx, accAddr) - _, _, err := k.bankKeeper.AddCoins(ctx, withdrawAddr, withdraw.TruncateDecimal()) + truncated, change := withdraw.TruncateDecimal() + feePool.CommunityPool = feePool.CommunityPool.Plus(change) + k.SetFeePool(ctx, feePool) + _, _, err := k.bankKeeper.AddCoins(ctx, withdrawAddr, truncated) if err != nil { panic(err) } + + return nil +} + +// 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)) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, ValidatorDistInfoKey) + defer iter.Close() + index := int64(0) + for ; iter.Valid(); iter.Next() { + var vdi types.ValidatorDistInfo + k.cdc.MustUnmarshalBinary(iter.Value(), &vdi) + if fn(index, vdi) { + return + } + index++ + } } diff --git a/x/distribution/simulation/invariants.go b/x/distribution/simulation/invariants.go new file mode 100644 index 000000000000..7036be46f0f9 --- /dev/null +++ b/x/distribution/simulation/invariants.go @@ -0,0 +1,17 @@ +package simulation + +import ( + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" +) + +// AllInvariants runs all invariants of the distribution module. +// Currently: total supply, positive power +func AllInvariants(ck bank.Keeper, k distribution.Keeper, am auth.AccountMapper) simulation.Invariant { + return func(app *baseapp.BaseApp) error { + return nil + } +} diff --git a/x/distribution/simulation/msgs.go b/x/distribution/simulation/msgs.go new file mode 100644 index 000000000000..241dccdababd --- /dev/null +++ b/x/distribution/simulation/msgs.go @@ -0,0 +1,130 @@ +package simulation + +import ( + "fmt" + "math/rand" + + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/mock" + "github.com/cosmos/cosmos-sdk/x/mock/simulation" +) + +// SimulateMsgSetWithdrawAddress +func SimulateMsgSetWithdrawAddress(m auth.AccountMapper, k distribution.Keeper) simulation.Operation { + handler := distribution.NewHandler(k) + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, + accs []simulation.Account, event func(string)) ( + action string, fOp []simulation.FutureOperation, err error) { + + accountOrigin := simulation.RandomAcc(r, accs) + accountDestination := simulation.RandomAcc(r, accs) + msg := distribution.NewMsgSetWithdrawAddress(accountOrigin.Address, accountDestination.Address) + + if msg.ValidateBasic() != nil { + return "", nil, fmt.Errorf("expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + } + + ctx, write := ctx.CacheContext() + result := handler(ctx, msg) + if result.IsOK() { + write() + } + + event(fmt.Sprintf("distribution/MsgSetWithdrawAddress/%v", result.IsOK())) + + action = fmt.Sprintf("TestMsgSetWithdrawAddress: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil, nil + } +} + +// SimulateMsgWithdrawDelegatorRewardsAll +func SimulateMsgWithdrawDelegatorRewardsAll(m auth.AccountMapper, k distribution.Keeper) simulation.Operation { + handler := distribution.NewHandler(k) + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, + accs []simulation.Account, event func(string)) ( + action string, fOp []simulation.FutureOperation, err error) { + + account := simulation.RandomAcc(r, accs) + msg := distribution.NewMsgWithdrawDelegatorRewardsAll(account.Address) + + if msg.ValidateBasic() != nil { + return "", nil, fmt.Errorf("expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + } + + ctx, write := ctx.CacheContext() + result := handler(ctx, msg) + if result.IsOK() { + write() + } + + event(fmt.Sprintf("distribution/MsgWithdrawDelegatorRewardsAll/%v", result.IsOK())) + + action = fmt.Sprintf("TestMsgWithdrawDelegatorRewardsAll: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil, nil + } +} + +// SimulateMsgWithdrawDelegatorReward +func SimulateMsgWithdrawDelegatorReward(m auth.AccountMapper, k distribution.Keeper) simulation.Operation { + handler := distribution.NewHandler(k) + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, + accs []simulation.Account, event func(string)) ( + action string, fOp []simulation.FutureOperation, err error) { + + delegatorAccount := simulation.RandomAcc(r, accs) + validatorAccount := simulation.RandomAcc(r, accs) + msg := distribution.NewMsgWithdrawDelegatorReward(delegatorAccount.Address, sdk.ValAddress(validatorAccount.Address)) + + if msg.ValidateBasic() != nil { + return "", nil, fmt.Errorf("expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + } + + ctx, write := ctx.CacheContext() + result := handler(ctx, msg) + if result.IsOK() { + write() + } + + event(fmt.Sprintf("distribution/MsgWithdrawDelegatorReward/%v", result.IsOK())) + + action = fmt.Sprintf("TestMsgWithdrawDelegatorReward: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil, nil + } +} + +// SimulateMsgWithdrawValidatorRewardsAll +func SimulateMsgWithdrawValidatorRewardsAll(m auth.AccountMapper, k distribution.Keeper) simulation.Operation { + handler := distribution.NewHandler(k) + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, + accs []simulation.Account, event func(string)) ( + action string, fOp []simulation.FutureOperation, err error) { + + account := simulation.RandomAcc(r, accs) + msg := distribution.NewMsgWithdrawValidatorRewardsAll(sdk.ValAddress(account.Address)) + + if msg.ValidateBasic() != nil { + return "", nil, fmt.Errorf("expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) + } + + ctx, write := ctx.CacheContext() + result := handler(ctx, msg) + if result.IsOK() { + write() + } + + event(fmt.Sprintf("distribution/MsgWithdrawValidatorRewardsAll/%v", result.IsOK())) + + action = fmt.Sprintf("TestMsgWithdrawValidatorRewardsAll: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + return action, nil, nil + } +} + +// Setup +// nolint: errcheck +func Setup(mapp *mock.App, k distribution.Keeper) simulation.RandSetup { + return func(r *rand.Rand, accs []simulation.Account) { + } +} diff --git a/x/distribution/types/dec_coin.go b/x/distribution/types/dec_coin.go index 59373976de71..9e93137881b7 100644 --- a/x/distribution/types/dec_coin.go +++ b/x/distribution/types/dec_coin.go @@ -43,9 +43,11 @@ func (coin DecCoin) Minus(coinB DecCoin) DecCoin { return DecCoin{coin.Denom, coin.Amount.Sub(coinB.Amount)} } -// return the decimal coins with trunctated decimals -func (coin DecCoin) TruncateDecimal() sdk.Coin { - return sdk.NewCoin(coin.Denom, coin.Amount.TruncateInt()) +// return the decimal coins with trunctated decimals, and return the change +func (coin DecCoin) TruncateDecimal() (sdk.Coin, DecCoin) { + truncated := coin.Amount.TruncateInt() + change := coin.Amount.Sub(sdk.NewDecFromInt(truncated)) + return sdk.NewCoin(coin.Denom, truncated), DecCoin{coin.Denom, change} } //_______________________________________________________________________ @@ -61,13 +63,16 @@ func NewDecCoins(coins sdk.Coins) DecCoins { return dcs } -// return the coins with trunctated decimals -func (coins DecCoins) TruncateDecimal() sdk.Coins { +// return the coins with trunctated decimals, and return the change +func (coins DecCoins) TruncateDecimal() (sdk.Coins, DecCoins) { + changeSum := DecCoins{} out := make(sdk.Coins, len(coins)) for i, coin := range coins { - out[i] = coin.TruncateDecimal() + truncated, change := coin.TruncateDecimal() + out[i] = truncated + changeSum = changeSum.Plus(DecCoins{change}) } - return out + return out, changeSum } // Plus combines two sets of coins @@ -147,3 +152,27 @@ func (coins DecCoins) QuoDec(d sdk.Dec) DecCoins { } return res } + +// returns the amount of a denom from deccoins +func (coins DecCoins) AmountOf(denom string) sdk.Dec { + switch len(coins) { + case 0: + return sdk.ZeroDec() + case 1: + coin := coins[0] + if coin.Denom == denom { + return coin.Amount + } + return sdk.ZeroDec() + default: + midIdx := len(coins) / 2 // 2:1, 3:1, 4:2 + coin := coins[midIdx] + if denom < coin.Denom { + return coins[:midIdx].AmountOf(denom) + } else if denom == coin.Denom { + return coin.Amount + } else { + return coins[midIdx+1:].AmountOf(denom) + } + } +} diff --git a/x/distribution/types/delegator_info.go b/x/distribution/types/delegator_info.go index f5b8978e5f24..4667d66e0009 100644 --- a/x/distribution/types/delegator_info.go +++ b/x/distribution/types/delegator_info.go @@ -21,7 +21,13 @@ func NewDelegationDistInfo(delegatorAddr sdk.AccAddress, valOperatorAddr sdk.Val } } -// withdraw rewards from delegator +// Withdraw rewards from delegator. +// Among many things, it does: +// * updates validator info's total del accum. +// * calls vi.TakeFeePoolRewards, which: +// * updates validator info's FeePoolWithdrawalHeight, thus setting accum to 0. +// * updates fee pool to latest height and total val accum w/ given totalBonded. +// (see comment on TakeFeePoolRewards for more info). func (di DelegationDistInfo) WithdrawRewards(fp FeePool, vi ValidatorDistInfo, height int64, totalBonded, vdTokens, totalDelShares, delegatorShares, commissionRate sdk.Dec) (DelegationDistInfo, ValidatorDistInfo, FeePool, DecCoins) { diff --git a/x/distribution/types/delegator_info_test.go b/x/distribution/types/delegator_info_test.go index 516cbf99dfa1..4af7f0a8f122 100644 --- a/x/distribution/types/delegator_info_test.go +++ b/x/distribution/types/delegator_info_test.go @@ -33,7 +33,7 @@ func TestWithdrawRewards(t *testing.T) { validatorTokens, validatorDelShares, di1Shares, commissionRate) assert.Equal(t, height, di1.WithdrawalHeight) - assert.True(sdk.DecEq(t, sdk.NewDec(900), fp.ValAccum.Accum)) + assert.True(sdk.DecEq(t, sdk.NewDec(900), fp.TotalValAccum.Accum)) assert.True(sdk.DecEq(t, sdk.NewDec(900), fp.Pool[0].Amount)) assert.True(sdk.DecEq(t, sdk.NewDec(49), vi.Pool[0].Amount)) assert.True(sdk.DecEq(t, sdk.NewDec(2), vi.PoolCommission[0].Amount)) @@ -48,7 +48,7 @@ func TestWithdrawRewards(t *testing.T) { validatorTokens, validatorDelShares, di2Shares, commissionRate) assert.Equal(t, height, di2.WithdrawalHeight) - assert.True(sdk.DecEq(t, sdk.NewDec(1800), fp.ValAccum.Accum)) + assert.True(sdk.DecEq(t, sdk.NewDec(1800), fp.TotalValAccum.Accum)) assert.True(sdk.DecEq(t, sdk.NewDec(1800), fp.Pool[0].Amount)) assert.True(sdk.DecEq(t, sdk.NewDec(49), vi.Pool[0].Amount)) assert.True(sdk.DecEq(t, sdk.NewDec(4), vi.PoolCommission[0].Amount)) diff --git a/x/distribution/types/errors.go b/x/distribution/types/errors.go index 57a3dd73e360..605c1b38db33 100644 --- a/x/distribution/types/errors.go +++ b/x/distribution/types/errors.go @@ -8,8 +8,9 @@ import ( type CodeType = sdk.CodeType const ( - DefaultCodespace sdk.CodespaceType = 6 - CodeInvalidInput CodeType = 103 + DefaultCodespace sdk.CodespaceType = 6 + CodeInvalidInput CodeType = 103 + CodeNoDistributionInfo CodeType = 104 ) func ErrNilDelegatorAddr(codespace sdk.CodespaceType) sdk.Error { @@ -21,3 +22,9 @@ func ErrNilWithdrawAddr(codespace sdk.CodespaceType) sdk.Error { func ErrNilValidatorAddr(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidInput, "validator address is nil") } +func ErrNoDelegationDistInfo(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeNoDistributionInfo, "no delegation distribution info") +} +func ErrNoValidatorDistInfo(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeNoDistributionInfo, "no validator distribution info") +} diff --git a/x/distribution/types/fee_pool.go b/x/distribution/types/fee_pool.go index 66731cb197a2..abd12f752f59 100644 --- a/x/distribution/types/fee_pool.go +++ b/x/distribution/types/fee_pool.go @@ -1,7 +1,10 @@ package types import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" + cmn "github.com/tendermint/tendermint/libs/common" ) // total accumulation tracker @@ -17,7 +20,7 @@ func NewTotalAccum(height int64) TotalAccum { } } -// update total validator accumulation factor for the new height +// update total accumulation factor for the new height // CONTRACT: height should be greater than the old height func (ta TotalAccum) UpdateForNewHeight(height int64, accumCreatedPerBlock sdk.Dec) TotalAccum { blocks := height - ta.UpdateHeight @@ -29,25 +32,65 @@ func (ta TotalAccum) UpdateForNewHeight(height int64, accumCreatedPerBlock sdk.D return ta } +// get total accumulation factor for the given height +// CONTRACT: height should be greater than the old height +func (ta TotalAccum) GetAccum(height int64, accumCreatedPerBlock sdk.Dec) sdk.Dec { + blocks := height - ta.UpdateHeight + if blocks < 0 { + panic("reverse updated for new height") + } + return ta.Accum.Add(accumCreatedPerBlock.MulInt(sdk.NewInt(blocks))) +} + +// update total validator accumulation factor for the new height +// CONTRACT: height should be greater than the old height +func (ta TotalAccum) UpdateForNewHeightDEBUG(height int64, accumCreatedPerBlock sdk.Dec) TotalAccum { + blocks := height - ta.UpdateHeight + if blocks < 0 { + panic("reverse updated for new height") + } + if !accumCreatedPerBlock.IsZero() && blocks != 0 { + fmt.Println( + cmn.Blue( + fmt.Sprintf("FP Add %v * %v = %v, + %v (old) => %v (new)", + accumCreatedPerBlock.String(), sdk.NewInt(blocks), + accumCreatedPerBlock.MulInt(sdk.NewInt(blocks)).String(), + ta.Accum.String(), + ta.Accum.Add(accumCreatedPerBlock.MulInt(sdk.NewInt(blocks))).String(), + ), + ), + ) + } + ta.Accum = ta.Accum.Add(accumCreatedPerBlock.MulInt(sdk.NewInt(blocks))) + ta.UpdateHeight = height + return ta +} + //___________________________________________________________________________________________ // global fee pool for distribution type FeePool struct { - ValAccum TotalAccum `json:"val_accum"` // total valdator accum held by validators + TotalValAccum TotalAccum `json:"val_accum"` // total valdator accum held by validators Pool DecCoins `json:"pool"` // funds for all validators which have yet to be withdrawn CommunityPool DecCoins `json:"community_pool"` // pool for community funds yet to be spent } // update total validator accumulation factor +// NOTE: Do not call this except from ValidatorDistInfo.TakeFeePoolRewards(). func (f FeePool) UpdateTotalValAccum(height int64, totalBondedTokens sdk.Dec) FeePool { - f.ValAccum = f.ValAccum.UpdateForNewHeight(height, totalBondedTokens) + f.TotalValAccum = f.TotalValAccum.UpdateForNewHeightDEBUG(height, totalBondedTokens) return f } +// get the total validator accum for the fee pool without modifying the state +func (f FeePool) GetTotalValAccum(height int64, totalBondedTokens sdk.Dec) sdk.Dec { + return f.TotalValAccum.GetAccum(height, totalBondedTokens) +} + // zero fee pool func InitialFeePool() FeePool { return FeePool{ - ValAccum: NewTotalAccum(0), + TotalValAccum: NewTotalAccum(0), Pool: DecCoins{}, CommunityPool: DecCoins{}, } diff --git a/x/distribution/types/fee_pool_test.go b/x/distribution/types/fee_pool_test.go index e39fb09c94c1..478ec7539baa 100644 --- a/x/distribution/types/fee_pool_test.go +++ b/x/distribution/types/fee_pool_test.go @@ -23,8 +23,8 @@ func TestUpdateTotalValAccum(t *testing.T) { fp := InitialFeePool() fp = fp.UpdateTotalValAccum(5, sdk.NewDec(3)) - require.True(sdk.DecEq(t, sdk.NewDec(15), fp.ValAccum.Accum)) + require.True(sdk.DecEq(t, sdk.NewDec(15), fp.TotalValAccum.Accum)) fp = fp.UpdateTotalValAccum(8, sdk.NewDec(2)) - require.True(sdk.DecEq(t, sdk.NewDec(21), fp.ValAccum.Accum)) + require.True(sdk.DecEq(t, sdk.NewDec(21), fp.TotalValAccum.Accum)) } diff --git a/x/distribution/types/keepers.go b/x/distribution/types/keepers.go index 31a68a5da9a6..5009c6b5af2b 100644 --- a/x/distribution/types/keepers.go +++ b/x/distribution/types/keepers.go @@ -10,6 +10,8 @@ type StakeKeeper interface { Validator(ctx sdk.Context, valAddr sdk.ValAddress) sdk.Validator ValidatorByConsAddr(ctx sdk.Context, consAddr sdk.ConsAddress) sdk.Validator TotalPower(ctx sdk.Context) sdk.Dec + GetLastTotalPower(ctx sdk.Context) sdk.Dec + GetLastValidatorPower(ctx sdk.Context, valAddr sdk.ValAddress) sdk.Dec } // expected coin keeper diff --git a/x/distribution/types/validator_info.go b/x/distribution/types/validator_info.go index c8a02569cba6..8e7b55c29c76 100644 --- a/x/distribution/types/validator_info.go +++ b/x/distribution/types/validator_info.go @@ -1,7 +1,10 @@ package types import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" + cmn "github.com/tendermint/tendermint/libs/common" ) // distribution info for a particular validator @@ -19,9 +22,9 @@ func NewValidatorDistInfo(operatorAddr sdk.ValAddress, currentHeight int64) Vali return ValidatorDistInfo{ OperatorAddr: operatorAddr, FeePoolWithdrawalHeight: currentHeight, - Pool: DecCoins{}, - PoolCommission: DecCoins{}, - DelAccum: NewTotalAccum(currentHeight), + Pool: DecCoins{}, + PoolCommission: DecCoins{}, + DelAccum: NewTotalAccum(currentHeight), } } @@ -31,27 +34,56 @@ func (vi ValidatorDistInfo) UpdateTotalDelAccum(height int64, totalDelShares sdk return vi } -// move any available accumulated fees in the FeePool to the validator's pool +// Get the calculated accum of this validator at the provided height +func (vi ValidatorDistInfo) GetAccum(height int64, vdTokens sdk.Dec) sdk.Dec { + blocks := height - vi.FeePoolWithdrawalHeight + return vdTokens.MulInt(sdk.NewInt(blocks)) +} + +// Move any available accumulated fees in the FeePool to the validator's pool. +// * updates validator info's FeePoolWithdrawalHeight, thus setting accum to 0. +// * updates fee pool to latest height and total val accum w/ given totalBonded. +// This is the only way to update the FeePool's validator TotalAccum. +// NOTE: This algorithm works as long as TakeFeePoolRewards is called after every power change. +// - called in ValidationDistInfo.WithdrawCommission. +// - called in DelegationDistInfo.WithdrawRewards. +// NOTE: When a delegator unbonds, say, onDelegationSharesModified -> +// WithdrawDelegationReward -> WithdrawRewards. func (vi ValidatorDistInfo) TakeFeePoolRewards(fp FeePool, height int64, totalBonded, vdTokens, commissionRate sdk.Dec) (ValidatorDistInfo, FeePool) { fp = fp.UpdateTotalValAccum(height, totalBonded) - if fp.ValAccum.Accum.IsZero() { + if fp.TotalValAccum.Accum.IsZero() { return vi, fp } // update the validators pool - blocks := height - vi.FeePoolWithdrawalHeight + accum := vi.GetAccum(height, vdTokens) vi.FeePoolWithdrawalHeight = height - accum := vdTokens.MulInt(sdk.NewInt(blocks)) - withdrawalTokens := fp.Pool.MulDec(accum).QuoDec(fp.ValAccum.Accum) + + if !accum.IsZero() { + fmt.Println( + cmn.Red( + fmt.Sprintf("FP Sub %v - %v => %v", + fp.TotalValAccum.Accum.String(), + accum.String(), + fp.TotalValAccum.Accum.Sub(accum).String(), + ), + ), + ) + } + + if accum.GT(fp.TotalValAccum.Accum) { + panic("individual accum should never be greater than the total") + } + withdrawalTokens := fp.Pool.MulDec(accum).QuoDec(fp.TotalValAccum.Accum) remainingTokens := fp.Pool.Minus(withdrawalTokens) commission := withdrawalTokens.MulDec(commissionRate) afterCommission := withdrawalTokens.Minus(commission) - fp.ValAccum.Accum = fp.ValAccum.Accum.Sub(accum) + fp.TotalValAccum.Accum = fp.TotalValAccum.Accum.Sub(accum) fp.Pool = remainingTokens vi.PoolCommission = vi.PoolCommission.Plus(commission) vi.Pool = vi.Pool.Plus(afterCommission) diff --git a/x/distribution/types/validator_info_test.go b/x/distribution/types/validator_info_test.go index afa6d8c76363..9d1e39fa6a06 100644 --- a/x/distribution/types/validator_info_test.go +++ b/x/distribution/types/validator_info_test.go @@ -29,13 +29,13 @@ func TestTakeFeePoolRewards(t *testing.T) { fp.Pool = DecCoins{NewDecCoin("stake", 1000)} vi1, fp = vi1.TakeFeePoolRewards(fp, height, totalBondedTokens, validatorTokens1, commissionRate1) - require.True(sdk.DecEq(t, sdk.NewDec(900), fp.ValAccum.Accum)) + require.True(sdk.DecEq(t, sdk.NewDec(900), fp.TotalValAccum.Accum)) assert.True(sdk.DecEq(t, sdk.NewDec(900), fp.Pool[0].Amount)) assert.True(sdk.DecEq(t, sdk.NewDec(100-2), vi1.Pool[0].Amount)) assert.True(sdk.DecEq(t, sdk.NewDec(2), vi1.PoolCommission[0].Amount)) vi2, fp = vi2.TakeFeePoolRewards(fp, height, totalBondedTokens, validatorTokens2, commissionRate2) - require.True(sdk.DecEq(t, sdk.NewDec(500), fp.ValAccum.Accum)) + require.True(sdk.DecEq(t, sdk.NewDec(500), fp.TotalValAccum.Accum)) assert.True(sdk.DecEq(t, sdk.NewDec(500), fp.Pool[0].Amount)) assert.True(sdk.DecEq(t, sdk.NewDec(400-12), vi2.Pool[0].Amount)) assert.True(sdk.DecEq(t, vi2.PoolCommission[0].Amount, sdk.NewDec(12))) @@ -45,7 +45,7 @@ func TestTakeFeePoolRewards(t *testing.T) { fp.Pool[0].Amount = fp.Pool[0].Amount.Add(sdk.NewDec(1000)) vi3, fp = vi3.TakeFeePoolRewards(fp, height, totalBondedTokens, validatorTokens3, commissionRate3) - require.True(sdk.DecEq(t, sdk.NewDec(500), fp.ValAccum.Accum)) + require.True(sdk.DecEq(t, sdk.NewDec(500), fp.TotalValAccum.Accum)) assert.True(sdk.DecEq(t, sdk.NewDec(500), fp.Pool[0].Amount)) assert.True(sdk.DecEq(t, sdk.NewDec(1000-40), vi3.Pool[0].Amount)) assert.True(sdk.DecEq(t, vi3.PoolCommission[0].Amount, sdk.NewDec(40))) @@ -67,7 +67,7 @@ func TestWithdrawCommission(t *testing.T) { // for a more fun staring condition, have an non-withdraw update vi, fp = vi.TakeFeePoolRewards(fp, height, totalBondedTokens, validatorTokens, commissionRate) - require.True(sdk.DecEq(t, sdk.NewDec(900), fp.ValAccum.Accum)) + require.True(sdk.DecEq(t, sdk.NewDec(900), fp.TotalValAccum.Accum)) assert.True(sdk.DecEq(t, sdk.NewDec(900), fp.Pool[0].Amount)) assert.True(sdk.DecEq(t, sdk.NewDec(100-2), vi.Pool[0].Amount)) assert.True(sdk.DecEq(t, sdk.NewDec(2), vi.PoolCommission[0].Amount)) @@ -77,7 +77,7 @@ func TestWithdrawCommission(t *testing.T) { fp.Pool[0].Amount = fp.Pool[0].Amount.Add(sdk.NewDec(1000)) vi, fp, commissionRecv := vi.WithdrawCommission(fp, height, totalBondedTokens, validatorTokens, commissionRate) - require.True(sdk.DecEq(t, sdk.NewDec(1800), fp.ValAccum.Accum)) + require.True(sdk.DecEq(t, sdk.NewDec(1800), fp.TotalValAccum.Accum)) assert.True(sdk.DecEq(t, sdk.NewDec(1800), fp.Pool[0].Amount)) assert.True(sdk.DecEq(t, sdk.NewDec(200-4), vi.Pool[0].Amount)) assert.Zero(t, len(vi.PoolCommission)) diff --git a/x/gov/tally.go b/x/gov/tally.go index c5751258a00a..b6e42c4b5a02 100644 --- a/x/gov/tally.go +++ b/x/gov/tally.go @@ -50,7 +50,7 @@ func tally(ctx sdk.Context, keeper Keeper, proposal Proposal) (passes bool, tall } else { keeper.ds.IterateDelegations(ctx, vote.Voter, func(index int64, delegation sdk.Delegation) (stop bool) { - valAddrStr := delegation.GetValidator().String() + valAddrStr := delegation.GetValidatorAddr().String() if val, ok := currValidators[valAddrStr]; ok { val.Minus = val.Minus.Add(delegation.GetShares()) diff --git a/x/mint/abci_app.go b/x/mint/abci_app.go index cb3bd44c51bf..73491f8081e1 100644 --- a/x/mint/abci_app.go +++ b/x/mint/abci_app.go @@ -21,5 +21,6 @@ func BeginBlocker(ctx sdk.Context, k Keeper) { 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)) k.SetMinter(ctx, minter) } diff --git a/x/mint/expected_keepers.go b/x/mint/expected_keepers.go index 8daaaf7ac32d..150e155cb3b5 100644 --- a/x/mint/expected_keepers.go +++ b/x/mint/expected_keepers.go @@ -1,6 +1,8 @@ package mint -import sdk "github.com/cosmos/cosmos-sdk/types" +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) // expected stake keeper type StakeKeeper interface { diff --git a/x/mint/genesis.go b/x/mint/genesis.go index 9dab64628ef3..5617685730b0 100644 --- a/x/mint/genesis.go +++ b/x/mint/genesis.go @@ -6,7 +6,7 @@ import ( // GenesisState - all distribution state that must be provided at genesis type GenesisState struct { - Minter Minter `json:"Minter"` // minter object + Minter Minter `json:"minter"` // minter object Params Params `json:"params"` // inflation params } diff --git a/x/mint/minter.go b/x/mint/minter.go index da2f6c5bed03..135675887bf0 100644 --- a/x/mint/minter.go +++ b/x/mint/minter.go @@ -40,6 +40,7 @@ func (m Minter) ProcessProvisions(params Params, totalSupply, bondedRatio sdk.De m.Inflation = m.NextInflation(params, bondedRatio) provisionsDec := m.Inflation.Mul(totalSupply).Quo(hrsPerYr) provisions = sdk.NewCoin(params.MintDenom, provisionsDec.TruncateInt()) + return m, provisions } diff --git a/x/mock/simulation/constants.go b/x/mock/simulation/constants.go index a96d4541f186..f0f53222f923 100644 --- a/x/mock/simulation/constants.go +++ b/x/mock/simulation/constants.go @@ -17,7 +17,7 @@ const ( evidenceFraction float64 = 0.5 // TODO Remove in favor of binary search for invariant violation - onOperation bool = false + onOperation bool = true ) var ( diff --git a/x/mock/simulation/random_simulate_blocks.go b/x/mock/simulation/random_simulate_blocks.go index e52ca1068c6c..bccb42285fc2 100644 --- a/x/mock/simulation/random_simulate_blocks.go +++ b/x/mock/simulation/random_simulate_blocks.go @@ -129,24 +129,38 @@ func SimulateFromSeed(tb testing.TB, app *baseapp.BaseApp, pastTimes = append(pastTimes, header.Time) pastVoteInfos = append(pastVoteInfos, request.LastCommitInfo.Votes) + // Construct log writer + logWriter := addLogMessage(testingMode, blockLogBuilders, i) + // Run the BeginBlock handler + logWriter("BeginBlock") app.BeginBlock(request) if testingMode { // Make sure invariants hold at beginning of block - assertAllInvariants(t, app, invariants, displayLogs) + assertAllInvariants(t, app, invariants, "BeginBlock", displayLogs) } - logWriter := addLogMessage(testingMode, blockLogBuilders, i) ctx := app.NewContext(false, header) thisBlockSize := getBlockSize(r, blockSize) // Run queued operations. Ignores blocksize if blocksize is too small + logWriter("Queued operations") numQueuedOpsRan := runQueuedOperations(operationQueue, int(header.Height), tb, r, app, ctx, accs, logWriter, displayLogs, event) numQueuedTimeOpsRan := runQueuedTimeOperations(timeOperationQueue, header.Time, tb, r, app, ctx, accs, logWriter, displayLogs, event) + if testingMode && onOperation { + // Make sure invariants hold at end of queued operations + assertAllInvariants(t, app, invariants, "QueuedOperations", displayLogs) + } + thisBlockSize = thisBlockSize - numQueuedOpsRan - numQueuedTimeOpsRan + logWriter("Standard operations") operations := blockSimulator(thisBlockSize, r, app, ctx, accs, header, logWriter) opCount += operations + numQueuedOpsRan + numQueuedTimeOpsRan + if testingMode { + // Make sure invariants hold at end of block + assertAllInvariants(t, app, invariants, "StandardOperations", displayLogs) + } res := app.EndBlock(abci.RequestEndBlock{}) header.Height++ @@ -156,7 +170,7 @@ func SimulateFromSeed(tb testing.TB, app *baseapp.BaseApp, if testingMode { // Make sure invariants hold at end of block - assertAllInvariants(t, app, invariants, displayLogs) + assertAllInvariants(t, app, invariants, "EndBlock", displayLogs) } if commit { app.Commit() @@ -210,7 +224,7 @@ func createBlockSimulator(testingMode bool, tb testing.TB, t *testing.T, event f queueOperations(operationQueue, timeOperationQueue, futureOps) if testingMode { if onOperation { - assertAllInvariants(t, app, invariants, displayLogs) + assertAllInvariants(t, app, invariants, fmt.Sprintf("operation: %v", logUpdate), displayLogs) } if opCount%50 == 0 { fmt.Printf("\rSimulating... block %d/%d, operation %d/%d. ", header.Height, totalNumBlocks, opCount, blocksize) @@ -370,6 +384,7 @@ func RandomRequestBeginBlock(r *rand.Rand, validators map[string]mockValidator, voteInfos[i] = abci.VoteInfo{ Validator: abci.Validator{ Address: pubkey.Address(), + Power: mVal.val.Power, }, SignedLastBlock: signed, } diff --git a/x/mock/simulation/util.go b/x/mock/simulation/util.go index 54bfabc73f8e..93c0a5be097e 100644 --- a/x/mock/simulation/util.go +++ b/x/mock/simulation/util.go @@ -102,10 +102,11 @@ func addLogMessage(testingmode bool, blockLogBuilders []*strings.Builder, height } // assertAllInvariants asserts a list of provided invariants against application state -func assertAllInvariants(t *testing.T, app *baseapp.BaseApp, invariants []Invariant, displayLogs func()) { +func assertAllInvariants(t *testing.T, app *baseapp.BaseApp, invariants []Invariant, where string, displayLogs func()) { for i := 0; i < len(invariants); i++ { err := invariants[i](app) if err != nil { + fmt.Printf("Invariants broken after %s\n", where) fmt.Println(err.Error()) displayLogs() t.Fatal() diff --git a/x/slashing/hooks.go b/x/slashing/hooks.go index ed07bc0c759a..3ad08b864b8c 100644 --- a/x/slashing/hooks.go +++ b/x/slashing/hooks.go @@ -30,7 +30,7 @@ func (k Keeper) onValidatorBonded(ctx sdk.Context, address sdk.ConsAddress) { } // Mark the slashing period as having ended when a validator begins unbonding -func (k Keeper) onValidatorBeginUnbonding(ctx sdk.Context, address sdk.ConsAddress) { +func (k Keeper) onValidatorBeginUnbonding(ctx sdk.Context, address sdk.ConsAddress, _ sdk.ValAddress) { slashingPeriod := k.getValidatorSlashingPeriodForHeight(ctx, address, ctx.BlockHeight()) slashingPeriod.EndHeight = ctx.BlockHeight() k.addOrUpdateValidatorSlashingPeriod(ctx, slashingPeriod) @@ -56,13 +56,13 @@ func (h Hooks) OnValidatorBonded(ctx sdk.Context, address sdk.ConsAddress) { } // Implements sdk.ValidatorHooks -func (h Hooks) OnValidatorBeginUnbonding(ctx sdk.Context, address sdk.ConsAddress) { - h.k.onValidatorBeginUnbonding(ctx, address) +func (h Hooks) OnValidatorBeginUnbonding(ctx sdk.Context, address sdk.ConsAddress, operator sdk.ValAddress) { + h.k.onValidatorBeginUnbonding(ctx, address, operator) } // nolint - unused hooks func (h Hooks) OnValidatorCreated(_ sdk.Context, _ sdk.ValAddress) {} -func (h Hooks) OnValidatorCommissionChange(_ sdk.Context, _ sdk.ValAddress) {} +func (h Hooks) OnValidatorModified(_ sdk.Context, _ sdk.ValAddress) {} func (h Hooks) OnValidatorRemoved(_ sdk.Context, _ sdk.ValAddress) {} func (h Hooks) OnDelegationCreated(_ sdk.Context, _ sdk.AccAddress, _ sdk.ValAddress) {} func (h Hooks) OnDelegationSharesModified(_ sdk.Context, _ sdk.AccAddress, _ sdk.ValAddress) {} diff --git a/x/slashing/hooks_test.go b/x/slashing/hooks_test.go index 951e3637f377..5da7ebafbb46 100644 --- a/x/slashing/hooks_test.go +++ b/x/slashing/hooks_test.go @@ -20,7 +20,7 @@ func TestHookOnValidatorBeginUnbonding(t *testing.T) { ctx, _, _, _, keeper := createTestInput(t, DefaultParams()) addr := sdk.ConsAddress(addrs[0]) keeper.onValidatorBonded(ctx, addr) - keeper.onValidatorBeginUnbonding(ctx, addr) + keeper.onValidatorBeginUnbonding(ctx, addr, addrs[0]) period := keeper.getValidatorSlashingPeriodForHeight(ctx, addr, ctx.BlockHeight()) require.Equal(t, ValidatorSlashingPeriod{addr, ctx.BlockHeight(), ctx.BlockHeight(), sdk.ZeroDec()}, period) } diff --git a/x/slashing/keeper.go b/x/slashing/keeper.go index 861391ac2a5f..4cd8ed91f397 100644 --- a/x/slashing/keeper.go +++ b/x/slashing/keeper.go @@ -37,7 +37,8 @@ func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, vs sdk.ValidatorSet, paramspa return keeper } -// handle a validator signing two blocks at the same height +// handle a validator signing two blocks at the same height. +// power: power of the double-signing validator at the height of infraction. func (k Keeper) handleDoubleSign(ctx sdk.Context, addr crypto.Address, infractionHeight int64, timestamp time.Time, power int64) { logger := ctx.Logger().With("module", "x/slashing") time := ctx.BlockHeader().Time @@ -70,7 +71,12 @@ func (k Keeper) handleDoubleSign(ctx sdk.Context, addr crypto.Address, infractio revisedFraction := k.capBySlashingPeriod(ctx, consAddr, fraction, distributionHeight) logger.Info(fmt.Sprintf("Fraction slashed capped by slashing period from %v to %v", fraction, revisedFraction)) - // Slash validator + // Slash validator. + // `power` is the int64 power of the validator as provided to/by + // Tendermint. This value is validator.Tokens as sent to Tendermint via + // ABCI, and now received as evidence. + // The revisedFraction (which is the new fraction to be slashed) is passed + // in separately to separately slash unbonding and rebonding delegations. k.validatorSet.Slash(ctx, consAddr, distributionHeight, power, revisedFraction) // Jail validator if not already jailed diff --git a/x/slashing/slashing_period.go b/x/slashing/slashing_period.go index 0595d5eeb25c..4b1328858882 100644 --- a/x/slashing/slashing_period.go +++ b/x/slashing/slashing_period.go @@ -37,6 +37,7 @@ func (k Keeper) capBySlashingPeriod(ctx sdk.Context, address sdk.ConsAddress, fr // This function retrieves the most recent slashing period starting // before a particular height - so the slashing period that was "in effect" // at the time of an infraction committed at that height. +// Slashing periods are created upon validator bonding. func (k Keeper) getValidatorSlashingPeriodForHeight(ctx sdk.Context, address sdk.ConsAddress, height int64) (slashingPeriod ValidatorSlashingPeriod) { store := ctx.KVStore(k.storeKey) // Get the most recent slashing period at or before the infraction height diff --git a/x/stake/genesis.go b/x/stake/genesis.go index ff8f59d44310..2fed877414ab 100644 --- a/x/stake/genesis.go +++ b/x/stake/genesis.go @@ -26,7 +26,6 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data types.GenesisState) (res [ keeper.SetPool(ctx, data.Pool) keeper.SetParams(ctx, data.Params) - keeper.InitIntraTxCounter(ctx) for i, validator := range data.Validators { validator.BondIntraTxCounter = int16(i) // set the intra-tx counter to the order the validators are presented diff --git a/x/stake/handler.go b/x/stake/handler.go index a75c30055ba0..24cbee71750d 100644 --- a/x/stake/handler.go +++ b/x/stake/handler.go @@ -114,8 +114,6 @@ func handleMsgCreateValidator(ctx sdk.Context, msg types.MsgCreateValidator, k k } k.OnValidatorCreated(ctx, validator.OperatorAddr) - accAddr := sdk.AccAddress(validator.OperatorAddr) - k.OnDelegationCreated(ctx, accAddr, validator.OperatorAddr) tags := sdk.NewTags( tags.Action, tags.ActionCreateValidator, @@ -150,6 +148,7 @@ func handleMsgEditValidator(ctx sdk.Context, msg types.MsgEditValidator, k keepe return err.Result() } validator.Commission = commission + k.OnValidatorModified(ctx, msg.ValidatorAddr) } k.SetValidator(ctx, validator) @@ -185,9 +184,6 @@ func handleMsgDelegate(ctx sdk.Context, msg types.MsgDelegate, k keeper.Keeper) return err.Result() } - // call the hook if present - k.OnDelegationCreated(ctx, msg.DelegatorAddr, validator.OperatorAddr) - tags := sdk.NewTags( tags.Action, tags.ActionDelegate, tags.Delegator, []byte(msg.DelegatorAddr.String()), diff --git a/x/stake/handler_test.go b/x/stake/handler_test.go index 3cd81202ae26..c4a558b91609 100644 --- a/x/stake/handler_test.go +++ b/x/stake/handler_test.go @@ -889,21 +889,21 @@ func TestUnbondingWhenExcessValidators(t *testing.T) { require.True(t, got.IsOK(), "expected no error on runMsgCreateValidator") // apply TM updates keeper.ApplyAndReturnValidatorSetUpdates(ctx) - require.Equal(t, 1, len(keeper.GetValidatorsBonded(ctx))) + require.Equal(t, 1, len(keeper.GetLastValidators(ctx))) msgCreateValidator = NewTestMsgCreateValidator(validatorAddr2, keep.PKs[1], 30) got = handleMsgCreateValidator(ctx, msgCreateValidator, keeper) require.True(t, got.IsOK(), "expected no error on runMsgCreateValidator") // apply TM updates keeper.ApplyAndReturnValidatorSetUpdates(ctx) - require.Equal(t, 2, len(keeper.GetValidatorsBonded(ctx))) + require.Equal(t, 2, len(keeper.GetLastValidators(ctx))) msgCreateValidator = NewTestMsgCreateValidator(validatorAddr3, keep.PKs[2], 10) got = handleMsgCreateValidator(ctx, msgCreateValidator, keeper) require.True(t, got.IsOK(), "expected no error on runMsgCreateValidator") // apply TM updates keeper.ApplyAndReturnValidatorSetUpdates(ctx) - require.Equal(t, 2, len(keeper.GetValidatorsBonded(ctx))) + require.Equal(t, 2, len(keeper.GetLastValidators(ctx))) // unbond the valdator-2 msgBeginUnbonding := NewMsgBeginUnbonding(sdk.AccAddress(validatorAddr2), validatorAddr2, sdk.NewDec(30)) @@ -916,7 +916,7 @@ func TestUnbondingWhenExcessValidators(t *testing.T) { // because there are extra validators waiting to get in, the queued // validator (aka. validator-1) should make it into the bonded group, thus // the total number of validators should stay the same - vals := keeper.GetValidatorsBonded(ctx) + vals := keeper.GetLastValidators(ctx) require.Equal(t, 2, len(vals), "vals %v", vals) val1, found := keeper.GetValidator(ctx, validatorAddr1) require.True(t, found) diff --git a/x/stake/keeper/delegation.go b/x/stake/keeper/delegation.go index 8bb468d99c46..a5d08d4892a9 100644 --- a/x/stake/keeper/delegation.go +++ b/x/stake/keeper/delegation.go @@ -359,6 +359,13 @@ func (k Keeper) Delegate(ctx sdk.Context, delAddr sdk.AccAddress, bondAmt sdk.Co } } + // call the appropriate hook if present + if found { + k.OnDelegationSharesModified(ctx, delAddr, validator.OperatorAddr) + } else { + k.OnDelegationCreated(ctx, delAddr, validator.OperatorAddr) + } + if subtractAccount { // Account new shares, save _, _, err = k.bankKeeper.SubtractCoins(ctx, delegation.DelegatorAddr, sdk.Coins{bondAmt}) @@ -373,6 +380,7 @@ func (k Keeper) Delegate(ctx sdk.Context, delAddr sdk.AccAddress, bondAmt sdk.Co delegation.Shares = delegation.Shares.Add(newShares) delegation.Height = ctx.BlockHeight() k.SetDelegation(ctx, delegation) + return newShares, nil } @@ -480,7 +488,14 @@ func (k Keeper) BeginUnbonding(ctx sdk.Context, return types.UnbondingDelegation{}, err } - balance := sdk.NewCoin(k.BondDenom(ctx), returnAmount.RoundInt()) + rounded := returnAmount.TruncateInt() + balance := sdk.NewCoin(k.BondDenom(ctx), rounded) + change := returnAmount.Sub(sdk.NewDecFromInt(rounded)) + + // for now, change is just burned + pool := k.GetPool(ctx) + pool.LooseTokens = pool.LooseTokens.Sub(change) + k.SetPool(ctx, pool) // no need to create the ubd object just complete now if completeNow { @@ -543,7 +558,15 @@ func (k Keeper) BeginRedelegation(ctx sdk.Context, delAddr sdk.AccAddress, return types.Redelegation{}, err } - returnCoin := sdk.Coin{k.BondDenom(ctx), returnAmount.RoundInt()} + rounded := returnAmount.TruncateInt() + returnCoin := sdk.NewCoin(k.BondDenom(ctx), rounded) + change := returnAmount.Sub(sdk.NewDecFromInt(rounded)) + + // for now, change is just burned + pool := k.GetPool(ctx) + pool.LooseTokens = pool.LooseTokens.Sub(change) + k.SetPool(ctx, pool) + dstValidator, found := k.GetValidator(ctx, valDstAddr) if !found { return types.Redelegation{}, types.ErrBadRedelegationDst(k.Codespace()) diff --git a/x/stake/keeper/hooks.go b/x/stake/keeper/hooks.go index 81bf5594ba75..14eedb6936fd 100644 --- a/x/stake/keeper/hooks.go +++ b/x/stake/keeper/hooks.go @@ -11,9 +11,9 @@ func (k Keeper) OnValidatorCreated(ctx sdk.Context, address sdk.ValAddress) { k.hooks.OnValidatorCreated(ctx, address) } } -func (k Keeper) OnValidatorCommissionChange(ctx sdk.Context, address sdk.ValAddress) { +func (k Keeper) OnValidatorModified(ctx sdk.Context, address sdk.ValAddress) { if k.hooks != nil { - k.hooks.OnValidatorCommissionChange(ctx, address) + k.hooks.OnValidatorModified(ctx, address) } } @@ -29,9 +29,9 @@ func (k Keeper) OnValidatorBonded(ctx sdk.Context, address sdk.ConsAddress) { } } -func (k Keeper) OnValidatorBeginUnbonding(ctx sdk.Context, address sdk.ConsAddress) { +func (k Keeper) OnValidatorBeginUnbonding(ctx sdk.Context, address sdk.ConsAddress, operator sdk.ValAddress) { if k.hooks != nil { - k.hooks.OnValidatorBeginUnbonding(ctx, address) + k.hooks.OnValidatorBeginUnbonding(ctx, address, operator) } } diff --git a/x/stake/keeper/keeper.go b/x/stake/keeper/keeper.go index 20927869866d..26c686bc9050 100644 --- a/x/stake/keeper/keeper.go +++ b/x/stake/keeper/keeper.go @@ -53,12 +53,12 @@ func (k Keeper) Codespace() sdk.CodespaceType { //_______________________________________________________________________ -// load/save the pool +// load the pool func (k Keeper) GetPool(ctx sdk.Context) (pool types.Pool) { store := ctx.KVStore(k.storeKey) b := store.Get(PoolKey) if b == nil { - panic("Stored pool should not have been nil") + panic("stored pool should not have been nil") } k.cdc.MustUnmarshalBinary(b, &pool) return @@ -71,21 +71,73 @@ func (k Keeper) SetPool(ctx sdk.Context, pool types.Pool) { store.Set(PoolKey, b) } -//__________________________________________________________________________ +//_______________________________________________________________________ -// get the current in-block validator operation counter -func (k Keeper) InitIntraTxCounter(ctx sdk.Context) { +// Load the last total validator power. +func (k Keeper) GetLastTotalPower(ctx sdk.Context) (power sdk.Dec) { store := ctx.KVStore(k.storeKey) - b := store.Get(IntraTxCounterKey) + b := store.Get(LastTotalPowerKey) if b == nil { - k.SetIntraTxCounter(ctx, 0) + panic("stored last total power should not have been nil") } + k.cdc.MustUnmarshalBinary(b, &power) + return } +// Set the last total validator power. +func (k Keeper) SetLastTotalPower(ctx sdk.Context, power sdk.Dec) { + if !power.IsInteger() { + panic("input power must be whole integer") + } + store := ctx.KVStore(k.storeKey) + b := k.cdc.MustMarshalBinary(power) + store.Set(LastTotalPowerKey, b) +} + +//_______________________________________________________________________ + +// Load the last validator power. +// Returns zero if the operator was not a validator last block. +func (k Keeper) GetLastValidatorPower(ctx sdk.Context, operator sdk.ValAddress) (power sdk.Dec) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(GetLastValidatorPowerKey(operator)) + if bz == nil { + return sdk.ZeroDec() + } + k.cdc.MustUnmarshalBinary(bz, &power) + return +} + +func (k Keeper) powerToBytes(power sdk.Dec) []byte { + bz := k.cdc.MustMarshalBinary(power) + return bz +} + +// Set the last validator power. +func (k Keeper) SetLastValidatorPower(ctx sdk.Context, operator sdk.ValAddress, power sdk.Dec) { + if !power.IsInteger() { + panic("input power must be whole integer") + } + store := ctx.KVStore(k.storeKey) + bz := k.powerToBytes(power) + store.Set(GetLastValidatorPowerKey(operator), bz) +} + +// Delete the last validator power. +func (k Keeper) DeleteLastValidatorPower(ctx sdk.Context, operator sdk.ValAddress) { + store := ctx.KVStore(k.storeKey) + store.Delete(GetLastValidatorPowerKey(operator)) +} + +//__________________________________________________________________________ + // get the current in-block validator operation counter func (k Keeper) GetIntraTxCounter(ctx sdk.Context) int16 { store := ctx.KVStore(k.storeKey) b := store.Get(IntraTxCounterKey) + if b == nil { + return 0 + } var counter int16 k.cdc.MustUnmarshalBinary(b, &counter) return counter diff --git a/x/stake/keeper/key.go b/x/stake/keeper/key.go index d5f2fc9d9e33..243fe34ec57c 100644 --- a/x/stake/keeper/key.go +++ b/x/stake/keeper/key.go @@ -15,21 +15,27 @@ var ( // Keys for store prefixes // TODO DEPRECATED: delete in next release and reorder keys // ParamKey = []byte{0x00} // key for parameters relating to staking - PoolKey = []byte{0x01} // key for the staking pools - ValidatorsKey = []byte{0x02} // prefix for each key to a validator - ValidatorsByConsAddrKey = []byte{0x03} // prefix for each key to a validator index, by pubkey - ValidatorsBondedIndexKey = []byte{0x04} // prefix for each key to a validator index, for bonded validators - ValidatorsByPowerIndexKey = []byte{0x05} // prefix for each key to a validator index, sorted by power - IntraTxCounterKey = []byte{0x06} // key for intra-block tx index - DelegationKey = []byte{0x07} // key for a delegation - UnbondingDelegationKey = []byte{0x08} // key for an unbonding-delegation - UnbondingDelegationByValIndexKey = []byte{0x09} // prefix for each key for an unbonding-delegation, by validator operator - RedelegationKey = []byte{0x0A} // key for a redelegation - RedelegationByValSrcIndexKey = []byte{0x0B} // prefix for each key for an redelegation, by source validator operator - RedelegationByValDstIndexKey = []byte{0x0C} // prefix for each key for an redelegation, by destination validator operator - UnbondingQueueKey = []byte{0x0D} // prefix for the timestamps in unbonding queue - RedelegationQueueKey = []byte{0x0E} // prefix for the timestamps in redelegations queue - ValidatorQueueKey = []byte{0x0F} // prefix for the timestamps in validator queue + PoolKey = []byte{0x01} // key for the staking pools + IntraTxCounterKey = []byte{0x02} // key for intra-block tx index + + // Last* values are const during a block. + LastValidatorPowerKey = []byte{0x11} // prefix for each key to a validator index, for bonded validators + LastTotalPowerKey = []byte{0x12} // prefix for the total power + + ValidatorsKey = []byte{0x21} // prefix for each key to a validator + ValidatorsByConsAddrKey = []byte{0x22} // prefix for each key to a validator index, by pubkey + ValidatorsByPowerIndexKey = []byte{0x23} // prefix for each key to a validator index, sorted by power + + DelegationKey = []byte{0x31} // key for a delegation + UnbondingDelegationKey = []byte{0x32} // key for an unbonding-delegation + UnbondingDelegationByValIndexKey = []byte{0x33} // prefix for each key for an unbonding-delegation, by validator operator + RedelegationKey = []byte{0x34} // key for a redelegation + RedelegationByValSrcIndexKey = []byte{0x35} // prefix for each key for an redelegation, by source validator operator + RedelegationByValDstIndexKey = []byte{0x36} // prefix for each key for an redelegation, by destination validator operator + + UnbondingQueueKey = []byte{0x41} // prefix for the timestamps in unbonding queue + RedelegationQueueKey = []byte{0x42} // prefix for the timestamps in redelegations queue + ValidatorQueueKey = []byte{0x43} // prefix for the timestamps in validator queue ) const maxDigitsForAccount = 12 // ~220,000,000 atoms created at launch @@ -46,9 +52,9 @@ func GetValidatorByConsAddrKey(addr sdk.ConsAddress) []byte { return append(ValidatorsByConsAddrKey, addr.Bytes()...) } -// Get the validator operator address from ValBondedIndexKey -func GetAddressFromValBondedIndexKey(IndexKey []byte) []byte { - return IndexKey[1:] // remove prefix bytes +// Get the validator operator address from LastValidatorPowerKey +func AddressFromLastValidatorPowerKey(key []byte) []byte { + return key[1:] // remove prefix bytes } // get the validator by power index. @@ -61,8 +67,8 @@ func GetValidatorsByPowerIndexKey(validator types.Validator, pool types.Pool) [] } // get the bonded validator index key for an operator address -func GetBondedValidatorIndexKey(operator sdk.ValAddress) []byte { - return append(ValidatorsBondedIndexKey, operator...) +func GetLastValidatorPowerKey(operator sdk.ValAddress) []byte { + return append(LastValidatorPowerKey, operator...) } // get the power ranking of a validator diff --git a/x/stake/keeper/key_test.go b/x/stake/keeper/key_test.go index dfd6a44e7990..385f313c15b5 100644 --- a/x/stake/keeper/key_test.go +++ b/x/stake/keeper/key_test.go @@ -35,10 +35,10 @@ func TestGetValidatorPowerRank(t *testing.T) { validator types.Validator wantHex string }{ - {val1, "050000000000000000ffffffffffffffffffff"}, - {val2, "050000000000000001ffffffffffffffffffff"}, - {val3, "05000000000000000affffffffffffffffffff"}, - {val4, "050000010000000000ffffffffffffffffffff"}, + {val1, "230000000000000000ffffffffffffffffffff"}, + {val2, "230000000000000001ffffffffffffffffffff"}, + {val3, "23000000000000000affffffffffffffffffff"}, + {val4, "230000010000000000ffffffffffffffffffff"}, } for i, tt := range tests { got := hex.EncodeToString(getValidatorPowerRank(tt.validator)) @@ -55,11 +55,11 @@ func TestGetREDByValDstIndexKey(t *testing.T) { wantHex string }{ {sdk.AccAddress(addr1), sdk.ValAddress(addr1), sdk.ValAddress(addr1), - "0c63d771218209d8bd03c482f69dfba57310f0860963d771218209d8bd03c482f69dfba57310f0860963d771218209d8bd03c482f69dfba57310f08609"}, + "3663d771218209d8bd03c482f69dfba57310f0860963d771218209d8bd03c482f69dfba57310f0860963d771218209d8bd03c482f69dfba57310f08609"}, {sdk.AccAddress(addr1), sdk.ValAddress(addr2), sdk.ValAddress(addr3), - "0c3ab62f0d93849be495e21e3e9013a517038f45bd63d771218209d8bd03c482f69dfba57310f086095ef3b5f25c54946d4a89fc0d09d2f126614540f2"}, + "363ab62f0d93849be495e21e3e9013a517038f45bd63d771218209d8bd03c482f69dfba57310f086095ef3b5f25c54946d4a89fc0d09d2f126614540f2"}, {sdk.AccAddress(addr2), sdk.ValAddress(addr1), sdk.ValAddress(addr3), - "0c3ab62f0d93849be495e21e3e9013a517038f45bd5ef3b5f25c54946d4a89fc0d09d2f126614540f263d771218209d8bd03c482f69dfba57310f08609"}, + "363ab62f0d93849be495e21e3e9013a517038f45bd5ef3b5f25c54946d4a89fc0d09d2f126614540f263d771218209d8bd03c482f69dfba57310f08609"}, } for i, tt := range tests { got := hex.EncodeToString(GetREDByValDstIndexKey(tt.delAddr, tt.valSrcAddr, tt.valDstAddr)) @@ -76,11 +76,11 @@ func TestGetREDByValSrcIndexKey(t *testing.T) { wantHex string }{ {sdk.AccAddress(addr1), sdk.ValAddress(addr1), sdk.ValAddress(addr1), - "0b63d771218209d8bd03c482f69dfba57310f0860963d771218209d8bd03c482f69dfba57310f0860963d771218209d8bd03c482f69dfba57310f08609"}, + "3563d771218209d8bd03c482f69dfba57310f0860963d771218209d8bd03c482f69dfba57310f0860963d771218209d8bd03c482f69dfba57310f08609"}, {sdk.AccAddress(addr1), sdk.ValAddress(addr2), sdk.ValAddress(addr3), - "0b5ef3b5f25c54946d4a89fc0d09d2f126614540f263d771218209d8bd03c482f69dfba57310f086093ab62f0d93849be495e21e3e9013a517038f45bd"}, + "355ef3b5f25c54946d4a89fc0d09d2f126614540f263d771218209d8bd03c482f69dfba57310f086093ab62f0d93849be495e21e3e9013a517038f45bd"}, {sdk.AccAddress(addr2), sdk.ValAddress(addr1), sdk.ValAddress(addr3), - "0b63d771218209d8bd03c482f69dfba57310f086095ef3b5f25c54946d4a89fc0d09d2f126614540f23ab62f0d93849be495e21e3e9013a517038f45bd"}, + "3563d771218209d8bd03c482f69dfba57310f086095ef3b5f25c54946d4a89fc0d09d2f126614540f23ab62f0d93849be495e21e3e9013a517038f45bd"}, } for i, tt := range tests { got := hex.EncodeToString(GetREDByValSrcIndexKey(tt.delAddr, tt.valSrcAddr, tt.valDstAddr)) diff --git a/x/stake/keeper/sdk_types.go b/x/stake/keeper/sdk_types.go index 667284356657..4e859a42a887 100644 --- a/x/stake/keeper/sdk_types.go +++ b/x/stake/keeper/sdk_types.go @@ -30,10 +30,10 @@ func (k Keeper) IterateValidators(ctx sdk.Context, fn func(index int64, validato // iterate through the active validator set and perform the provided function func (k Keeper) IterateValidatorsBonded(ctx sdk.Context, fn func(index int64, validator sdk.Validator) (stop bool)) { store := ctx.KVStore(k.storeKey) - iterator := sdk.KVStorePrefixIterator(store, ValidatorsBondedIndexKey) + iterator := sdk.KVStorePrefixIterator(store, LastValidatorPowerKey) i := int64(0) for ; iterator.Valid(); iterator.Next() { - address := GetAddressFromValBondedIndexKey(iterator.Key()) + address := AddressFromLastValidatorPowerKey(iterator.Key()) validator, found := k.GetValidator(ctx, address) if !found { panic(fmt.Sprintf("validator record not found for address: %v\n", address)) @@ -66,7 +66,7 @@ func (k Keeper) ValidatorByConsAddr(ctx sdk.Context, addr sdk.ConsAddress) sdk.V return val } -// total power from the bond +// total power from the bond (not last, but current) func (k Keeper) TotalPower(ctx sdk.Context) sdk.Dec { pool := k.GetPool(ctx) return pool.BondedTokens diff --git a/x/stake/keeper/slash.go b/x/stake/keeper/slash.go index af59726aefe5..294ae9a9fd82 100644 --- a/x/stake/keeper/slash.go +++ b/x/stake/keeper/slash.go @@ -51,6 +51,7 @@ func (k Keeper) Slash(ctx sdk.Context, consAddr sdk.ConsAddress, infractionHeigh } operatorAddress := validator.GetOperator() + k.OnValidatorModified(ctx, operatorAddress) // Track remaining slash amount for the validator // This will decrease when we slash unbondings and @@ -97,10 +98,13 @@ func (k Keeper) Slash(ctx sdk.Context, consAddr sdk.ConsAddress, infractionHeigh // cannot decrease balance below zero tokensToBurn := sdk.MinDec(remainingSlashAmount, validator.Tokens) + tokensToBurn = sdk.MaxDec(tokensToBurn, sdk.ZeroDec()) // defensive. - // burn validator's tokens and update the validator + // Deduct from validator's bonded tokens and update the validator. + // The deducted tokens are returned to pool.LooseTokens. validator = k.RemoveValidatorTokens(ctx, validator, tokensToBurn) pool := k.GetPool(ctx) + // Burn the slashed tokens, which are now loose. pool.LooseTokens = pool.LooseTokens.Sub(tokensToBurn) k.SetPool(ctx, pool) diff --git a/x/stake/keeper/test_common.go b/x/stake/keeper/test_common.go index d0ac4a2825c0..13e2ca2c595e 100644 --- a/x/stake/keeper/test_common.go +++ b/x/stake/keeper/test_common.go @@ -106,7 +106,6 @@ func CreateTestInput(t *testing.T, isCheckTx bool, initCoins int64) (sdk.Context keeper := NewKeeper(cdc, keyStake, tkeyStake, ck, pk.Subspace(DefaultParamspace), types.DefaultCodespace) keeper.SetPool(ctx, types.InitialPool()) keeper.SetParams(ctx, types.DefaultParams()) - keeper.InitIntraTxCounter(ctx) // fill all the addresses with some coins, set the loose pool tokens simultaneously for _, addr := range Addrs { diff --git a/x/stake/keeper/val_state_change.go b/x/stake/keeper/val_state_change.go index 89d3f681faf6..cfa381973d30 100644 --- a/x/stake/keeper/val_state_change.go +++ b/x/stake/keeper/val_state_change.go @@ -11,7 +11,14 @@ import ( "github.com/cosmos/cosmos-sdk/x/stake/types" ) -// Apply and return accumulated updates to the bonded validator set +// Apply and return accumulated updates to the bonded validator set. Also, +// * Updates the active valset as keyed by LastValidatorPowerKey. +// * Updates the total power as keyed by LastTotalPowerKey. +// * Updates validator status' according to updated powers. +// * Updates the fee pool bonded vs loose tokens. +// * Updates relevant indices. +// It gets called once after genesis, another time maybe after genesis transactions, +// then once at every EndBlock. // // CONTRACT: Only validators with non-zero power or zero-power that were bonded // at the previous block height or were removed from the validator set entirely @@ -20,11 +27,14 @@ func (k Keeper) ApplyAndReturnValidatorSetUpdates(ctx sdk.Context) (updates []ab store := ctx.KVStore(k.storeKey) maxValidators := k.GetParams(ctx).MaxValidators + totalPower := int64(0) - // retrieve last validator set - last := k.retrieveLastValidatorSet(ctx) + // Retrieve the last validator set. + // The persistent set is updated later in this function. + // (see LastValidatorPowerKey). + last := k.getLastValidatorsByAddr(ctx) - // iterate over validators, highest power to lowest + // Iterate over validators, highest power to lowest. iterator := sdk.KVStoreReversePrefixIterator(store, ValidatorsByPowerIndexKey) count := 0 for ; iterator.Valid() && count < int(maxValidators); iterator.Next() { @@ -62,22 +72,22 @@ func (k Keeper) ApplyAndReturnValidatorSetUpdates(ctx sdk.Context) (updates []ab oldPowerBytes, found := last[operatorBytes] // calculate the new power bytes - newPowerBytes := validator.ABCIValidatorPowerBytes(k.cdc) - + newPower := validator.BondedTokens().RoundInt64() + newPowerBytes := k.powerToBytes(sdk.NewDec(newPower)) // update the validator set if power has changed if !found || !bytes.Equal(oldPowerBytes, newPowerBytes) { updates = append(updates, validator.ABCIValidatorUpdate()) + + // set validator power on lookup index. + k.SetLastValidatorPower(ctx, operator, sdk.NewDec(newPower)) } // validator still in the validator set, so delete from the copy delete(last, operatorBytes) - // set the bonded validator index - store.Set(GetBondedValidatorIndexKey(operator), newPowerBytes) - // keep count count++ - + totalPower += newPower } // sort the no-longer-bonded validators @@ -98,11 +108,15 @@ func (k Keeper) ApplyAndReturnValidatorSetUpdates(ctx sdk.Context) (updates []ab } // delete from the bonded validator index - store.Delete(GetBondedValidatorIndexKey(operator)) + k.DeleteLastValidatorPower(ctx, operator) // update the validator set updates = append(updates, validator.ABCIValidatorUpdateZero()) + } + // set total power on lookup index if there are any updates + if len(updates) > 0 { + k.SetLastTotalPower(ctx, sdk.NewDec(totalPower)) } return updates @@ -219,7 +233,7 @@ func (k Keeper) beginUnbondingValidator(ctx sdk.Context, validator types.Validat // call the unbond hook if present if k.hooks != nil { - k.hooks.OnValidatorBeginUnbonding(ctx, validator.ConsAddress()) + k.hooks.OnValidatorBeginUnbonding(ctx, validator.ConsAddress(), validator.OperatorAddr) } return validator @@ -237,11 +251,11 @@ func (k Keeper) completeUnbondingValidator(ctx sdk.Context, validator types.Vali // map of operator addresses to serialized power type validatorsByAddr map[[sdk.AddrLen]byte][]byte -// retrieve the last validator set -func (k Keeper) retrieveLastValidatorSet(ctx sdk.Context) validatorsByAddr { +// get the last validator set +func (k Keeper) getLastValidatorsByAddr(ctx sdk.Context) validatorsByAddr { last := make(validatorsByAddr) store := ctx.KVStore(k.storeKey) - iterator := sdk.KVStorePrefixIterator(store, ValidatorsBondedIndexKey) + iterator := sdk.KVStorePrefixIterator(store, LastValidatorPowerKey) for ; iterator.Valid(); iterator.Next() { var operator [sdk.AddrLen]byte copy(operator[:], iterator.Key()[1:]) diff --git a/x/stake/keeper/validator.go b/x/stake/keeper/validator.go index 95e5f0001283..9fd7434d313a 100644 --- a/x/stake/keeper/validator.go +++ b/x/stake/keeper/validator.go @@ -235,24 +235,24 @@ func (k Keeper) GetValidators(ctx sdk.Context, maxRetrieve uint16) (validators [ } // get the group of the bonded validators -func (k Keeper) GetValidatorsBonded(ctx sdk.Context) (validators []types.Validator) { +func (k Keeper) GetLastValidators(ctx sdk.Context) (validators []types.Validator) { store := ctx.KVStore(k.storeKey) // add the actual validator power sorted store maxValidators := k.MaxValidators(ctx) validators = make([]types.Validator, maxValidators) - iterator := sdk.KVStorePrefixIterator(store, ValidatorsBondedIndexKey) + iterator := sdk.KVStorePrefixIterator(store, LastValidatorPowerKey) defer iterator.Close() i := 0 for ; iterator.Valid(); iterator.Next() { // sanity check - if i > int(maxValidators-1) { - panic("maxValidators is less than the number of records in ValidatorsBonded Store, store should have been updated") + if i >= int(maxValidators) { + panic("more validators than maxValidators found") } - address := GetAddressFromValBondedIndexKey(iterator.Key()) + address := AddressFromLastValidatorPowerKey(iterator.Key()) validator := k.mustGetValidator(ctx, address) validators[i] = validator @@ -261,7 +261,7 @@ func (k Keeper) GetValidatorsBonded(ctx sdk.Context) (validators []types.Validat return validators[:i] // trim } -// get the group of bonded validators sorted by power-rank +// get the current group of bonded validators sorted by power-rank func (k Keeper) GetBondedValidatorsByPower(ctx sdk.Context) []types.Validator { store := ctx.KVStore(k.storeKey) maxValidators := k.MaxValidators(ctx) diff --git a/x/stake/keeper/validator_test.go b/x/stake/keeper/validator_test.go index 6f14ba7a5da3..7acf1cc02dae 100644 --- a/x/stake/keeper/validator_test.go +++ b/x/stake/keeper/validator_test.go @@ -49,7 +49,7 @@ func TestSetValidator(t *testing.T) { assert.True(ValEq(t, validator, resVal)) require.True(t, found) - resVals := keeper.GetValidatorsBonded(ctx) + resVals := keeper.GetLastValidators(ctx) require.Equal(t, 1, len(resVals)) assert.True(ValEq(t, validator, resVals[0])) @@ -191,7 +191,7 @@ func TestSlashToZeroPowerRemoved(t *testing.T) { require.False(t, found) } -// This function tests UpdateValidator, GetValidator, GetValidatorsBonded, RemoveValidator +// This function tests UpdateValidator, GetValidator, GetLastValidators, RemoveValidator func TestValidatorBasics(t *testing.T) { ctx, _, keeper := CreateTestInput(t, false, 1000) pool := keeper.GetPool(ctx) @@ -213,7 +213,7 @@ func TestValidatorBasics(t *testing.T) { // check the empty keeper first _, found := keeper.GetValidator(ctx, addrVals[0]) require.False(t, found) - resVals := keeper.GetValidatorsBonded(ctx) + resVals := keeper.GetLastValidators(ctx) require.Zero(t, len(resVals)) resVals = keeper.GetValidators(ctx, 2) @@ -237,7 +237,7 @@ func TestValidatorBasics(t *testing.T) { require.True(t, found) assert.True(ValEq(t, validators[0], resVal)) - resVals = keeper.GetValidatorsBonded(ctx) + resVals = keeper.GetLastValidators(ctx) require.Equal(t, 1, len(resVals)) assert.True(ValEq(t, validators[0], resVals[0])) assert.Equal(t, sdk.Bonded, validators[0].Status) @@ -255,7 +255,7 @@ func TestValidatorBasics(t *testing.T) { require.True(t, found) assert.True(ValEq(t, validators[0], resVal)) - resVals = keeper.GetValidatorsBonded(ctx) + resVals = keeper.GetLastValidators(ctx) require.Equal(t, 1, len(resVals)) assert.True(ValEq(t, validators[0], resVals[0])) @@ -269,7 +269,7 @@ func TestValidatorBasics(t *testing.T) { require.True(t, found) assert.True(ValEq(t, validators[2], resVal)) - resVals = keeper.GetValidatorsBonded(ctx) + resVals = keeper.GetLastValidators(ctx) require.Equal(t, 3, len(resVals)) assert.True(ValEq(t, validators[0], resVals[0])) // order doesn't matter here assert.True(ValEq(t, validators[1], resVals[1])) diff --git a/x/stake/simulation/invariants.go b/x/stake/simulation/invariants.go index 2866f6292fef..e249bac9f41d 100644 --- a/x/stake/simulation/invariants.go +++ b/x/stake/simulation/invariants.go @@ -7,6 +7,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/distribution" "github.com/cosmos/cosmos-sdk/x/mock/simulation" "github.com/cosmos/cosmos-sdk/x/stake" abci "github.com/tendermint/tendermint/abci/types" @@ -14,9 +15,9 @@ import ( // AllInvariants runs all invariants of the stake module. // Currently: total supply, positive power -func AllInvariants(ck bank.Keeper, k stake.Keeper, am auth.AccountMapper) simulation.Invariant { +func AllInvariants(ck bank.Keeper, k stake.Keeper, f auth.FeeCollectionKeeper, d distribution.Keeper, am auth.AccountMapper) simulation.Invariant { return func(app *baseapp.BaseApp) error { - err := SupplyInvariants(ck, k, am)(app) + err := SupplyInvariants(ck, k, f, d, am)(app) if err != nil { return err } @@ -31,19 +32,19 @@ func AllInvariants(ck bank.Keeper, k stake.Keeper, am auth.AccountMapper) simula // SupplyInvariants checks that the total supply reflects all held loose tokens, bonded tokens, and unbonding delegations // nolint: unparam -func SupplyInvariants(ck bank.Keeper, k stake.Keeper, am auth.AccountMapper) simulation.Invariant { +func SupplyInvariants(ck bank.Keeper, k stake.Keeper, f auth.FeeCollectionKeeper, d distribution.Keeper, am auth.AccountMapper) simulation.Invariant { return func(app *baseapp.BaseApp) error { ctx := app.NewContext(false, abci.Header{}) pool := k.GetPool(ctx) - loose := sdk.ZeroInt() + loose := sdk.ZeroDec() bonded := sdk.ZeroDec() am.IterateAccounts(ctx, func(acc auth.Account) bool { - loose = loose.Add(acc.GetCoins().AmountOf("steak")) + loose = loose.Add(sdk.NewDecFromInt(acc.GetCoins().AmountOf("steak"))) return false }) k.IterateUnbondingDelegations(ctx, func(_ int64, ubd stake.UnbondingDelegation) bool { - loose = loose.Add(ubd.Balance.Amount) + loose = loose.Add(sdk.NewDecFromInt(ubd.Balance.Amount)) return false }) k.IterateValidators(ctx, func(_ int64, validator sdk.Validator) bool { @@ -51,24 +52,41 @@ func SupplyInvariants(ck bank.Keeper, k stake.Keeper, am auth.AccountMapper) sim case sdk.Bonded: bonded = bonded.Add(validator.GetPower()) case sdk.Unbonding: - loose = loose.Add(validator.GetTokens().RoundInt()) + loose = loose.Add(validator.GetTokens()) case sdk.Unbonded: - loose = loose.Add(validator.GetTokens().RoundInt()) + loose = loose.Add(validator.GetTokens()) } return false }) + feePool := d.GetFeePool(ctx) + + // add outstanding fees + loose = loose.Add(sdk.NewDecFromInt(f.GetCollectedFees(ctx).AmountOf("steak"))) + + // add community pool + loose = loose.Add(feePool.CommunityPool.AmountOf("steak")) + + // add validator distribution pool + loose = loose.Add(feePool.Pool.AmountOf("steak")) + + // add validator distribution commission and yet-to-be-withdrawn-by-delegators + d.IterateValidatorDistInfos(ctx, func(_ int64, distInfo distribution.ValidatorDistInfo) (stop bool) { + loose = loose.Add(distInfo.Pool.AmountOf("steak")) + loose = loose.Add(distInfo.PoolCommission.AmountOf("steak")) + return false + }) + // Loose tokens should equal coin supply plus unbonding delegations plus tokens on unbonded validators - if pool.LooseTokens.RoundInt64() != loose.Int64() { - return fmt.Errorf("expected loose tokens to equal total steak held by accounts - pool.LooseTokens: %v, sum of account tokens: %v", pool.LooseTokens.RoundInt64(), loose.Int64()) + if !pool.LooseTokens.Equal(loose) { + return fmt.Errorf("expected loose tokens to equal total steak held by accounts - pool.LooseTokens: %v, sum of account tokens: %v", pool.LooseTokens, loose) } // Bonded tokens should equal sum of tokens with bonded validators - if pool.BondedTokens.RoundInt64() != bonded.RoundInt64() { - return fmt.Errorf("expected bonded tokens to equal total steak held by bonded validators - pool.BondedTokens: %v, sum of bonded validator tokens: %v", pool.BondedTokens.RoundInt64(), bonded.RoundInt64()) + if !pool.BondedTokens.Equal(bonded) { + return fmt.Errorf("expected bonded tokens to equal total steak held by bonded validators - pool.BondedTokens: %v, sum of bonded validator tokens: %v", pool.BondedTokens, bonded) } - // TODO Inflation check on total supply return nil } } diff --git a/x/stake/simulation/sim_test.go b/x/stake/simulation/sim_test.go index 6aa780113939..2ce9fa00df6a 100644 --- a/x/stake/simulation/sim_test.go +++ b/x/stake/simulation/sim_test.go @@ -8,7 +8,9 @@ import ( abci "github.com/tendermint/tendermint/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/distribution" "github.com/cosmos/cosmos-sdk/x/mock" "github.com/cosmos/cosmos-sdk/x/mock/simulation" "github.com/cosmos/cosmos-sdk/x/params" @@ -22,13 +24,17 @@ func TestStakeWithRandomMessages(t *testing.T) { bank.RegisterCodec(mapp.Cdc) mapper := mapp.AccountMapper bankKeeper := bank.NewBaseKeeper(mapper) + feeKey := sdk.NewKVStoreKey("fee") stakeKey := sdk.NewKVStoreKey("stake") stakeTKey := sdk.NewTransientStoreKey("transient_stake") paramsKey := sdk.NewKVStoreKey("params") paramsTKey := sdk.NewTransientStoreKey("transient_params") + distrKey := sdk.NewKVStoreKey("distr") - paramstore := params.NewKeeper(mapp.Cdc, paramsKey, paramsTKey).Subspace(stake.DefaultParamspace) - stakeKeeper := stake.NewKeeper(mapp.Cdc, stakeKey, stakeTKey, bankKeeper, paramstore, stake.DefaultCodespace) + feeCollectionKeeper := auth.NewFeeCollectionKeeper(mapp.Cdc, feeKey) + paramstore := params.NewKeeper(mapp.Cdc, paramsKey, paramsTKey) + stakeKeeper := stake.NewKeeper(mapp.Cdc, stakeKey, stakeTKey, bankKeeper, paramstore.Subspace(stake.DefaultParamspace), stake.DefaultCodespace) + distrKeeper := distribution.NewKeeper(mapp.Cdc, distrKey, paramstore.Subspace(distribution.DefaultParamspace), bankKeeper, stakeKeeper, feeCollectionKeeper, distribution.DefaultCodespace) mapp.Router().AddRoute("stake", stake.NewHandler(stakeKeeper)) mapp.SetEndBlocker(func(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { validatorUpdates := stake.EndBlocker(ctx, stakeKeeper) @@ -58,7 +64,7 @@ func TestStakeWithRandomMessages(t *testing.T) { }, []simulation.RandSetup{ Setup(mapp, stakeKeeper), }, []simulation.Invariant{ - AllInvariants(bankKeeper, stakeKeeper, mapp.AccountMapper), + AllInvariants(bankKeeper, stakeKeeper, feeCollectionKeeper, distrKeeper, mapp.AccountMapper), }, 10, 100, false, ) diff --git a/x/stake/stake.go b/x/stake/stake.go index c4aa547022d8..c755e352aa47 100644 --- a/x/stake/stake.go +++ b/x/stake/stake.go @@ -39,12 +39,13 @@ var ( GetDelegationKey = keeper.GetDelegationKey GetDelegationsKey = keeper.GetDelegationsKey PoolKey = keeper.PoolKey + IntraTxCounterKey = keeper.IntraTxCounterKey + LastValidatorPowerKey = keeper.LastValidatorPowerKey + LastTotalPowerKey = keeper.LastTotalPowerKey ValidatorsKey = keeper.ValidatorsKey ValidatorsByConsAddrKey = keeper.ValidatorsByConsAddrKey - ValidatorsBondedIndexKey = keeper.ValidatorsBondedIndexKey ValidatorsByPowerIndexKey = keeper.ValidatorsByPowerIndexKey DelegationKey = keeper.DelegationKey - IntraTxCounterKey = keeper.IntraTxCounterKey GetUBDKey = keeper.GetUBDKey GetUBDByValIndexKey = keeper.GetUBDByValIndexKey GetUBDsKey = keeper.GetUBDsKey diff --git a/x/stake/types/delegation.go b/x/stake/types/delegation.go index 1b51e00acf19..95c2965abe84 100644 --- a/x/stake/types/delegation.go +++ b/x/stake/types/delegation.go @@ -104,9 +104,9 @@ func (d Delegation) Equal(d2 Delegation) bool { var _ sdk.Delegation = Delegation{} // nolint - for sdk.Delegation -func (d Delegation) GetDelegator() sdk.AccAddress { return d.DelegatorAddr } -func (d Delegation) GetValidator() sdk.ValAddress { return d.ValidatorAddr } -func (d Delegation) GetShares() sdk.Dec { return d.Shares } +func (d Delegation) GetDelegatorAddr() sdk.AccAddress { return d.DelegatorAddr } +func (d Delegation) GetValidatorAddr() sdk.ValAddress { return d.ValidatorAddr } +func (d Delegation) GetShares() sdk.Dec { return d.Shares } // HumanReadableString returns a human readable string representation of a // Delegation. An error is returned if the Delegation's delegator or validator diff --git a/x/stake/types/validator.go b/x/stake/types/validator.go index d774f761bd36..52d30f0a313b 100644 --- a/x/stake/types/validator.go +++ b/x/stake/types/validator.go @@ -314,12 +314,6 @@ func (v Validator) ABCIValidatorUpdate() abci.ValidatorUpdate { } } -// ABCIValidatorPowerBytes -func (v Validator) ABCIValidatorPowerBytes(cdc *codec.Codec) []byte { - power := v.BondedTokens().RoundInt64() - return cdc.MustMarshalBinary(power) -} - // ABCIValidatorUpdateZero returns an abci.ValidatorUpdate from a staked validator type // with zero power used for validator updates. func (v Validator) ABCIValidatorUpdateZero() abci.ValidatorUpdate {