diff --git a/x/validator-vesting/abci.go b/x/validator-vesting/abci.go index 2467cb65..ed5a567b 100644 --- a/x/validator-vesting/abci.go +++ b/x/validator-vesting/abci.go @@ -16,12 +16,12 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k keeper.Keeper) previousBlockTime = k.GetPreviousBlockTime(ctx) } - currentBlockTime := req.Header.GetTime() + currentBlockTime := ctx.BlockTime() var voteInfos VoteInfos - voteInfos = ctx.VoteInfos() + voteInfos = req.LastCommitInfo.GetVotes() validatorVestingKeys := k.GetAllAccountKeys(ctx) for _, key := range validatorVestingKeys { - acc := k.GetAccountFromAuthKeeper(ctx, key) + acc := k.GetAccountFromAuthKeeper(ctx, key[1:]) if voteInfos.ContainsValidatorAddress(acc.ValidatorAddress) { vote := voteInfos.MustFilterByValidatorAddress(acc.ValidatorAddress) if !vote.SignedLastBlock { @@ -36,14 +36,15 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k keeper.Keeper) } // check if a period ended in the last block - endTimes := k.GetPeriodEndTimes(ctx, key) + endTimes := k.GetPeriodEndTimes(ctx, key[1:]) + for i, t := range endTimes { if currentBlockTime.Unix() >= t && previousBlockTime.Unix() < t { - k.UpdateVestedCoinsProgress(ctx, key, i) + k.UpdateVestedCoinsProgress(ctx, key[1:], i) } } // handle any new/remaining debt on the account - k.HandleVestingDebt(ctx, key, currentBlockTime) + k.HandleVestingDebt(ctx, key[1:], currentBlockTime) } k.SetPreviousBlockTime(ctx, currentBlockTime) } diff --git a/x/validator-vesting/abci_test.go b/x/validator-vesting/abci_test.go new file mode 100644 index 00000000..4c865775 --- /dev/null +++ b/x/validator-vesting/abci_test.go @@ -0,0 +1,237 @@ +package validatorvesting + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + tmtime "github.com/tendermint/tendermint/types/time" + + "github.com/cosmos/cosmos-sdk/x/staking" + stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported" + "github.com/cosmos/cosmos-sdk/x/validator-vesting/internal/keeper" +) + +func TestBeginBlockerSignedBlock(t *testing.T) { + ctx, ak, _, stakingKeeper, _, vvk := keeper.CreateTestInput(t, false, 1000) + now := tmtime.Now() + + vva := keeper.ValidatorVestingDelegatorTestAccount(now) + + ak.SetAccount(ctx, vva) + delTokens := sdk.TokensFromConsensusPower(30) + vvk.SetValidatorVestingAccountKey(ctx, vva.Address) + + keeper.CreateValidators(ctx, stakingKeeper, []int64{5, 5, 5}) + + val1, found := stakingKeeper.GetValidator(ctx, keeper.ValOpAddr1) + require.True(t, found) + _, err := stakingKeeper.Delegate(ctx, vva.Address, delTokens, sdk.Unbonded, val1, true) + require.NoError(t, err) + + _ = staking.EndBlocker(ctx, stakingKeeper) + + // require that there exists one delegation + var delegations int + stakingKeeper.IterateDelegations(ctx, vva.Address, func(index int64, d stakingexported.DelegationI) (stop bool) { + delegations++ + return false + }) + + require.Equal(t, 1, delegations) + + val := abci.Validator{ + Address: val1.ConsPubKey.Address(), + Power: val1.ConsensusPower(), + } + + vva.ValidatorAddress = val1.ConsAddress() + ak.SetAccount(ctx, vva) + + height := int64(0) + blockTime := now + + addHour := func(t time.Time) time.Time { return t.Add(1 * time.Hour) } + + header := abci.Header{Height: height, Time: addHour(blockTime)} + + // mark the validator as having signed + req := abci.RequestBeginBlock{ + Header: header, + LastCommitInfo: abci.LastCommitInfo{ + Votes: []abci.VoteInfo{{ + Validator: val, + SignedLastBlock: true, + }}, + }, + } + + BeginBlocker(ctx, req, vvk) + height++ + blockTime = addHour(blockTime) + vva = vvk.GetAccountFromAuthKeeper(ctx, vva.Address) + require.Equal(t, []int64{0, 1}, vva.MissingSignCount) + + header = abci.Header{Height: height, Time: addHour(blockTime)} + + // mark the validator as having missed + req = abci.RequestBeginBlock{ + Header: header, + LastCommitInfo: abci.LastCommitInfo{ + Votes: []abci.VoteInfo{{ + Validator: val, + SignedLastBlock: false, + }}, + }, + } + + BeginBlocker(ctx, req, vvk) + height++ + blockTime = addHour(blockTime) + vva = vvk.GetAccountFromAuthKeeper(ctx, vva.Address) + require.Equal(t, []int64{1, 2}, vva.MissingSignCount) + + // mark the validator as being absent + req = abci.RequestBeginBlock{ + Header: header, + LastCommitInfo: abci.LastCommitInfo{ + Votes: []abci.VoteInfo{{ + Validator: abci.Validator{}, + SignedLastBlock: true, + }}, + }, + } + + BeginBlocker(ctx, req, vvk) + height++ + blockTime = addHour(blockTime) + vva = vvk.GetAccountFromAuthKeeper(ctx, vva.Address) + require.Equal(t, []int64{2, 3}, vva.MissingSignCount) +} + +func TestBeginBlockerSuccessfulPeriod(t *testing.T) { + height := int64(0) + now := tmtime.Now() + blockTime := now + numBlocks := int64(14) + addHour := func(t time.Time) time.Time { return t.Add(1 * time.Hour) } + ctx, ak, _, stakingKeeper, _, vvk := keeper.CreateTestInput(t, false, 1000) + + vva := keeper.ValidatorVestingDelegatorTestAccount(now) + + ak.SetAccount(ctx, vva) + vvk.SetValidatorVestingAccountKey(ctx, vva.Address) + + keeper.CreateValidators(ctx, stakingKeeper, []int64{5, 5, 5}) + + val1, found := stakingKeeper.GetValidator(ctx, keeper.ValOpAddr1) + require.True(t, found) + + _ = staking.EndBlocker(ctx, stakingKeeper) + + val := abci.Validator{ + Address: val1.ConsPubKey.Address(), + Power: val1.ConsensusPower(), + } + + vva.ValidatorAddress = val1.ConsAddress() + ak.SetAccount(ctx, vva) + + for ; height < numBlocks; height++ { + header := abci.Header{Height: height, Time: addHour(blockTime)} + // mark the validator as having signed + req := abci.RequestBeginBlock{ + Header: header, + LastCommitInfo: abci.LastCommitInfo{ + Votes: []abci.VoteInfo{{ + Validator: val, + SignedLastBlock: true, + }}, + }, + } + ctx = ctx.WithBlockHeader(header) + BeginBlocker(ctx, req, vvk) + blockTime = addHour(blockTime) + + if height == 11 { + vva = vvk.GetAccountFromAuthKeeper(ctx, vva.Address) + // require that missing sign count is set back to zero after the period increments. + require.Equal(t, []int64{0, 0}, vva.MissingSignCount) + } + + } + + vva = vvk.GetAccountFromAuthKeeper(ctx, vva.Address) + // t.Log(vva.MarshalYAML()) + require.Equal(t, [][]int{[]int{1, 1}, []int{0, 0}, []int{0, 0}}, vva.VestingPeriodProgress) +} + +func TestBeginBlockerUnsuccessfulPeriod(t *testing.T) { + height := int64(0) + now := tmtime.Now() + blockTime := now + numBlocks := int64(12) + addHour := func(t time.Time) time.Time { return t.Add(1 * time.Hour) } + + ctx, ak, _, stakingKeeper, _, vvk := keeper.CreateTestInput(t, false, 1000) + keeper.CreateValidators(ctx, stakingKeeper, []int64{5, 5, 5}) + + vva := keeper.ValidatorVestingDelegatorTestAccount(now) + + ak.SetAccount(ctx, vva) + delTokens := sdk.TokensFromConsensusPower(60) + vvk.SetValidatorVestingAccountKey(ctx, vva.Address) + + val1, found := stakingKeeper.GetValidator(ctx, keeper.ValOpAddr1) + require.True(t, found) + _, err := stakingKeeper.Delegate(ctx, vva.Address, delTokens, sdk.Unbonded, val1, true) + require.NoError(t, err) + + _ = staking.EndBlocker(ctx, stakingKeeper) + + // note that delegation modifies the account's state! + vva = vvk.GetAccountFromAuthKeeper(ctx, vva.Address) + + val := abci.Validator{ + Address: val1.ConsPubKey.Address(), + Power: val1.ConsensusPower(), + } + + vva.ValidatorAddress = val1.ConsAddress() + ak.SetAccount(ctx, vva) + + // run one period's worth of blocks + for ; height < numBlocks; height++ { + header := abci.Header{Height: height, Time: addHour(blockTime)} + // mark the validator as having missed + req := abci.RequestBeginBlock{ + Header: header, + LastCommitInfo: abci.LastCommitInfo{ + Votes: []abci.VoteInfo{{ + Validator: val, + SignedLastBlock: false, + }}, + }, + } + ctx = ctx.WithBlockHeader(header) + BeginBlocker(ctx, req, vvk) + blockTime = addHour(blockTime) + vva = vvk.GetAccountFromAuthKeeper(ctx, vva.Address) + } + + vva = vvk.GetAccountFromAuthKeeper(ctx, vva.Address) + // check that the period was unsucessful + require.Equal(t, [][]int{[]int{1, 0}, []int{0, 0}, []int{0, 0}}, vva.VestingPeriodProgress) + // check that there is debt after the period. + require.Equal(t, sdk.Coins{sdk.NewInt64Coin("stake", 30000000)}, vva.DebtAfterFailedVesting) + + var delegations int + stakingKeeper.IterateDelegations(ctx, vva.Address, func(index int64, d stakingexported.DelegationI) (stop bool) { + delegations++ + return false + }) + // require that all delegations were unbonded + require.Equal(t, 0, delegations) +} diff --git a/x/validator-vesting/alias.go b/x/validator-vesting/alias.go index 5a9fcfdf..5685f88d 100644 --- a/x/validator-vesting/alias.go +++ b/x/validator-vesting/alias.go @@ -1,6 +1,7 @@ -// nolint package validatorvesting +// nolint +// DONTCOVER import ( "github.com/cosmos/cosmos-sdk/x/validator-vesting/internal/keeper" "github.com/cosmos/cosmos-sdk/x/validator-vesting/internal/types" @@ -18,6 +19,7 @@ var ( DefaultGenesisState = types.DefaultGenesisState RegisterCodec = types.RegisterCodec ValidatorVestingAccountPrefix = types.ValidatorVestingAccountPrefix + BlocktimeKey = types.BlocktimeKey ValidatorVestingAccountKey = types.ValidatorVestingAccountKey NewKeeper = keeper.NewKeeper ) diff --git a/x/validator-vesting/genesis.go b/x/validator-vesting/genesis.go index b21fa3c1..e5f2bf9b 100644 --- a/x/validator-vesting/genesis.go +++ b/x/validator-vesting/genesis.go @@ -7,7 +7,7 @@ import ( ) // InitGenesis stores the account address of each ValidatorVestingAccount in the validator vesting keeper, for faster lookup. -// CONTRACT: Accounts created by the account keeper must have already been initialized/created +// CONTRACT: Accounts created by the account keeper must have already been initialized/created by AccountKeeper func InitGenesis(ctx sdk.Context, keeper Keeper, accountKeeper types.AccountKeeper, data GenesisState) { data.Accounts = auth.SanitizeGenesisAccounts(data.Accounts) for _, a := range data.Accounts { diff --git a/x/validator-vesting/internal/keeper/keeper.go b/x/validator-vesting/internal/keeper/keeper.go index b30c14ce..ab5bb512 100644 --- a/x/validator-vesting/internal/keeper/keeper.go +++ b/x/validator-vesting/internal/keeper/keeper.go @@ -11,11 +11,6 @@ import ( "github.com/tendermint/tendermint/libs/log" ) -var ( - // BlocktimeKey key for the time of the previous block - BlocktimeKey = []byte{0x00} -) - // Keeper of the validatorvesting store type Keeper struct { storeKey sdk.StoreKey @@ -47,7 +42,7 @@ func (k Keeper) Logger(ctx sdk.Context) log.Logger { // GetPreviousBlockTime get the blocktime for the previous block func (k Keeper) GetPreviousBlockTime(ctx sdk.Context) (blockTime time.Time) { store := ctx.KVStore(k.storeKey) - b := store.Get(BlocktimeKey) + b := store.Get(types.BlocktimeKey) if b == nil { panic("Previous block time not set") } @@ -59,78 +54,7 @@ func (k Keeper) GetPreviousBlockTime(ctx sdk.Context) (blockTime time.Time) { func (k Keeper) SetPreviousBlockTime(ctx sdk.Context, blockTime time.Time) { store := ctx.KVStore(k.storeKey) b := k.cdc.MustMarshalBinaryLengthPrefixed(blockTime) - store.Set(BlocktimeKey, b) -} - -// UpdateMissingSignCount increments the count of blocks missed during the current period -func (k Keeper) UpdateMissingSignCount(ctx sdk.Context, addr sdk.AccAddress, missedBlock bool) { - vv := k.GetAccountFromAuthKeeper(ctx, addr) - if missedBlock { - vv.MissingSignCount[0]++ - } - vv.MissingSignCount[1]++ - k.ak.SetAccount(ctx, vv) -} - -// UpdateVestedCoinsProgress sets the VestingPeriodProgress variable (0 = coins did not vest for the period, 1 = coins did vest for the period) for the given address and period. If coins did not vest, those coins are added to DebtAfterFailedVesting. Finally, MissingSignCount is reset to [0,0], representing that the next period has started and no blocks have been missed. -func (k Keeper) UpdateVestedCoinsProgress(ctx sdk.Context, addr sdk.AccAddress, period int) { - vv := k.GetAccountFromAuthKeeper(ctx, addr) - - threshold := sdk.NewDec(vv.SigningThreshold) - blocksMissed := sdk.NewDec(vv.MissingSignCount[0]) - blockCount := sdk.NewDec(vv.MissingSignCount[1]) - blocksSigned := blockCount.Sub(blocksMissed) - percentageBlocksSigned := blocksSigned.Quo(blockCount).Mul(sdk.NewDec(100)) - successfulVest := percentageBlocksSigned.GTE(threshold) - if successfulVest { - vv.VestingPeriodProgress[period] = 1 - } else { - vv.VestingPeriodProgress[period] = 0 - notVestedTokens := vv.VestingPeriods[period].VestingAmount - // add the tokens that did not vest to DebtAfterFailedVesting - vv.DebtAfterFailedVesting = vv.DebtAfterFailedVesting.Add(notVestedTokens) - } - // reset the number of missed blocks and total number of blocks in the period to zero - vv.MissingSignCount = []int64{0, 0} - k.ak.SetAccount(ctx, vv) -} - -// HandleVestingDebt removes coins after a vesting period in which the vesting -// threshold was not met. Sends/Burns tokens if there is enough spendable tokens, -// otherwise unbonds all existing tokens. -func (k Keeper) HandleVestingDebt(ctx sdk.Context, addr sdk.AccAddress, blockTime time.Time) { - vv := k.GetAccountFromAuthKeeper(ctx, addr) - remainingDebt := !vv.DebtAfterFailedVesting.IsZero() - if remainingDebt { - spendableCoins := vv.SpendableCoins(blockTime) - if spendableCoins.IsAllGTE(vv.DebtAfterFailedVesting) { - if vv.ReturnAddress != nil { - err := k.bk.SendCoins(ctx, addr, vv.ReturnAddress, vv.DebtAfterFailedVesting) - if err != nil { - panic(err) - } - } else { - err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, addr, types.ModuleName, vv.DebtAfterFailedVesting) - if err != nil { - panic(err) - } - err = k.supplyKeeper.BurnCoins(ctx, types.ModuleName, vv.DebtAfterFailedVesting) - if err != nil { - panic(err) - } - } - vv.DebtAfterFailedVesting = sdk.NewCoins() - k.ak.SetAccount(ctx, vv) - } else { - // iterate over all delegations made from the validator vesting account and undelegate - // note that we cannot safely undelegate only an amount of shares that covers the debt, - // because the value of those shares could change if a validator gets slashed - k.stakingKeeper.IterateDelegations(ctx, vv.Address, func(index int64, d stakingexported.DelegationI) (stop bool) { - k.stakingKeeper.Undelegate(ctx, d.GetDelegatorAddr(), d.GetValidatorAddr(), d.GetShares()) - return false - }) - } - } + store.Set(types.BlocktimeKey, b) } // SetValidatorVestingAccountKey stores the account key in the store. This is useful for when we want to iterate over all ValidatorVestingAcounts, so we can avoid iterating over any other accounts stored in the auth keeper. @@ -175,6 +99,84 @@ func (k Keeper) GetAccountFromAuthKeeper(ctx sdk.Context, addr sdk.AccAddress) * panic("validator vesting account not found") } +// UpdateMissingSignCount increments the count of blocks missed during the current period +func (k Keeper) UpdateMissingSignCount(ctx sdk.Context, addr sdk.AccAddress, missedBlock bool) { + vv := k.GetAccountFromAuthKeeper(ctx, addr) + if missedBlock { + vv.MissingSignCount[0]++ + } + vv.MissingSignCount[1]++ + k.ak.SetAccount(ctx, vv) +} + +// UpdateVestedCoinsProgress sets the VestingPeriodProgress variable (0 = coins did not vest for the period, 1 = coins did vest for the period) for the given address and period. If coins did not vest, those coins are added to DebtAfterFailedVesting. Finally, MissingSignCount is reset to [0,0], representing that the next period has started and no blocks have been missed. +func (k Keeper) UpdateVestedCoinsProgress(ctx sdk.Context, addr sdk.AccAddress, period int) { + vv := k.GetAccountFromAuthKeeper(ctx, addr) + + threshold := sdk.NewDec(vv.SigningThreshold) + blocksMissed := sdk.NewDec(vv.MissingSignCount[0]) + blockCount := sdk.NewDec(vv.MissingSignCount[1]) + var successfulVest bool + if blockCount.IsZero() { + successfulVest = true + } else { + blocksSigned := blockCount.Sub(blocksMissed) + percentageBlocksSigned := blocksSigned.Quo(blockCount).Mul(sdk.NewDec(100)) + successfulVest = percentageBlocksSigned.GTE(threshold) + } + + if successfulVest { + vv.VestingPeriodProgress[period][1] = 1 + } else { + vv.VestingPeriodProgress[period][1] = 0 + notVestedTokens := vv.VestingPeriods[period].VestingAmount + // add the tokens that did not vest to DebtAfterFailedVesting + vv.DebtAfterFailedVesting = vv.DebtAfterFailedVesting.Add(notVestedTokens) + } + vv.VestingPeriodProgress[period][0] = 1 + // reset the number of missed blocks and total number of blocks in the period to zero + vv.MissingSignCount = []int64{0, 0} + k.ak.SetAccount(ctx, vv) +} + +// HandleVestingDebt removes coins after a vesting period in which the vesting +// threshold was not met. Sends/Burns tokens if there is enough spendable tokens, +// otherwise unbonds all existing tokens. +func (k Keeper) HandleVestingDebt(ctx sdk.Context, addr sdk.AccAddress, blockTime time.Time) { + vv := k.GetAccountFromAuthKeeper(ctx, addr) + remainingDebt := !vv.DebtAfterFailedVesting.IsZero() + if remainingDebt { + spendableCoins := vv.SpendableCoins(blockTime) + if spendableCoins.IsAllGTE(vv.DebtAfterFailedVesting) { + if vv.ReturnAddress != nil { + err := k.bk.SendCoins(ctx, addr, vv.ReturnAddress, vv.DebtAfterFailedVesting) + if err != nil { + panic(err) + } + } else { + err := k.supplyKeeper.SendCoinsFromAccountToModule(ctx, addr, types.ModuleName, vv.DebtAfterFailedVesting) + if err != nil { + panic(err) + } + err = k.supplyKeeper.BurnCoins(ctx, types.ModuleName, vv.DebtAfterFailedVesting) + if err != nil { + panic(err) + } + } + vv.DebtAfterFailedVesting = sdk.NewCoins() + k.ak.SetAccount(ctx, vv) + } else { + // iterate over all delegations made from the validator vesting account and undelegate + // note that we cannot safely undelegate only an amount of shares that covers the debt, + // because the value of those shares could change if a validator gets slashed. + k.stakingKeeper.IterateDelegations(ctx, vv.Address, func(index int64, d stakingexported.DelegationI) (stop bool) { + k.stakingKeeper.Undelegate(ctx, d.GetDelegatorAddr(), d.GetValidatorAddr(), d.GetShares()) + return false + }) + } + } +} + // GetPeriodEndTimes returns an array of the times when each period ends func (k Keeper) GetPeriodEndTimes(ctx sdk.Context, addr sdk.AccAddress) []int64 { var endTimes []int64 diff --git a/x/validator-vesting/internal/keeper/keeper_test.go b/x/validator-vesting/internal/keeper/keeper_test.go index fe2bee7e..3f73f32f 100644 --- a/x/validator-vesting/internal/keeper/keeper_test.go +++ b/x/validator-vesting/internal/keeper/keeper_test.go @@ -1,14 +1,7 @@ package keeper -// TODO -// 1. Test that a signed block by the ValidatorAddress increases the blocks counter, but not the missed blocks counter -// 2. Test that an unsigned block increass both the blocks counter and the missed blocks counter -// 3. Test that the previous block time gets updated at the end of each begin block -// 4. Test that the block before a pivital block doesn't reset the period -// 5. Test that a pivotal block results in coins being vested successfully if the treshold is met -// 6. Test that a pivotal block results in HandleVestingDebt - import ( + "bytes" "testing" "time" @@ -42,6 +35,28 @@ func TestGetSetValidatorVestingAccounts(t *testing.T) { // require that GetAllAccountKeys returns one account keys := keeper.GetAllAccountKeys(ctx) require.Equal(t, 1, len(keys)) + for _, k := range keys { + require.NotPanics(t, func() { keeper.GetAccountFromAuthKeeper(ctx, k[1:]) }) + } + + vvAccounts := ValidatorVestingTestAccounts(10) + for _, a := range vvAccounts { + ak.SetAccount(ctx, a) + keeper.SetValidatorVestingAccountKey(ctx, a.Address) + } + + keys = keeper.GetAllAccountKeys(ctx) + require.Equal(t, 10, len(keys)) + + var ikeys [][]byte + keeper.IterateAccountKeys(ctx, func(accountKey []byte) bool { + if bytes.Equal(accountKey, keys[0]) { + ikeys = append(ikeys, accountKey) + return true + } + return false + }) + require.Equal(t, 1, len(ikeys)) } func TestGetSetPreviousBlock(t *testing.T) { @@ -60,6 +75,26 @@ func TestGetSetPreviousBlock(t *testing.T) { } +func TestGetEndTImes(t *testing.T) { + ctx, ak, _, _, _, keeper := CreateTestInput(t, false, 1000) + + now := tmtime.Now() + + vva := ValidatorVestingDelegatorTestAccount(now) + ak.SetAccount(ctx, vva) + keeper.SetValidatorVestingAccountKey(ctx, vva.Address) + + expectedEndTimes := []int64{ + now.Add(12 * time.Hour).Unix(), + now.Add(18 * time.Hour).Unix(), + now.Add(24 * time.Hour).Unix(), + } + + endTimes := keeper.GetPeriodEndTimes(ctx, vva.Address) + + require.Equal(t, expectedEndTimes, endTimes) +} + func TestSetMissingSignCount(t *testing.T) { ctx, ak, _, _, _, keeper := CreateTestInput(t, false, 1000) @@ -91,7 +126,7 @@ func TestUpdateVestedCoinsProgress(t *testing.T) { ak.SetAccount(ctx, vva) // require all vesting period tracking variables to be zero after validator vesting account is initialized - require.Equal(t, []int{0, 0, 0}, vva.VestingPeriodProgress) + require.Equal(t, [][]int{{0, 0}, {0, 0}, {0, 0}}, vva.VestingPeriodProgress) // period 0 passes with all blocks signed vva.MissingSignCount[0] = 0 @@ -103,13 +138,31 @@ func TestUpdateVestedCoinsProgress(t *testing.T) { // require that debt is zero require.Equal(t, sdk.Coins(nil), vva.DebtAfterFailedVesting) // require that the first vesting progress variable is 1 - require.Equal(t, []int{1, 0, 0}, vva.VestingPeriodProgress) + require.Equal(t, [][]int{{1, 1}, {0, 0}, {0, 0}}, vva.VestingPeriodProgress) + + // require that the missing block counter has reset + require.Equal(t, []int64{0, 0}, vva.MissingSignCount) + + vva = ValidatorVestingTestAccount() + ak.SetAccount(ctx, vva) + // period 0 passes with no blocks signed + // this is an edge case that shouldn't happen, + // the vest is considered successful in this case. + vva.MissingSignCount[0] = 0 + vva.MissingSignCount[1] = 0 + ak.SetAccount(ctx, vva) + vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address) + keeper.UpdateVestedCoinsProgress(ctx, vva.Address, 0) + vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address) + // require that debt is zero + require.Equal(t, sdk.Coins(nil), vva.DebtAfterFailedVesting) + // require that the first vesting progress variable is 1 + require.Equal(t, [][]int{{1, 1}, {0, 0}, {0, 0}}, vva.VestingPeriodProgress) // require that the missing block counter has reset require.Equal(t, []int64{0, 0}, vva.MissingSignCount) vva = ValidatorVestingTestAccount() - // Add the validator vesting account to the auth store ak.SetAccount(ctx, vva) // period 0 passes with 50% of blocks signed (below threshold) vva.MissingSignCount[0] = 50 @@ -119,14 +172,16 @@ func TestUpdateVestedCoinsProgress(t *testing.T) { vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address) // require that period 1 coins have become debt require.Equal(t, sdk.NewCoins(sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)), vva.DebtAfterFailedVesting) - // require that the first vesting progress variable is 0 - require.Equal(t, []int{0, 0, 0}, vva.VestingPeriodProgress) + // require that the first vesting progress variable is {1,0} + require.Equal(t, [][]int{{1, 0}, {0, 0}, {0, 0}}, vva.VestingPeriodProgress) // require that the missing block counter has reset require.Equal(t, []int64{0, 0}, vva.MissingSignCount) } -func TestHandleVestingDebtForcedUnbond(t *testing.T) { - ctx, ak, _, stakingKeeper, _, keeper := CreateTestInput(t, false, 1000) +func TestHandleVestingDebtNoDebt(t *testing.T) { + // ctx, ak, bk, stakingKeeper, supplyKeeper, keeper := CreateTestInput(t, false, 1000) + + ctx, ak, _, _, _, keeper := CreateTestInput(t, false, 1000) vva := ValidatorVestingTestAccount() // Delegate all coins @@ -144,13 +199,21 @@ func TestHandleVestingDebtForcedUnbond(t *testing.T) { require.Nil(t, vva.DelegatedFree) require.Nil(t, vva.GetCoins()) +} + +func TestHandleVestingDebtForcedUnbond(t *testing.T) { + // ctx, ak, bk, stakingKeeper, supplyKeeper, keeper := CreateTestInput(t, false, 1000) + + ctx, ak, _, stakingKeeper, _, keeper := CreateTestInput(t, false, 1000) + now := tmtime.Now() + // Create validators and a delegation from the validator vesting account CreateValidators(ctx, stakingKeeper, []int64{5, 5, 5}) - vva = ValidatorVestingDelegatorTestAccount(now) + vva := ValidatorVestingDelegatorTestAccount(now) ak.SetAccount(ctx, vva) - delTokens := sdk.TokensFromConsensusPower(30) - val1, found := stakingKeeper.GetValidator(ctx, valOpAddr1) + delTokens := sdk.TokensFromConsensusPower(60) + val1, found := stakingKeeper.GetValidator(ctx, ValOpAddr1) require.True(t, found) _, err := stakingKeeper.Delegate(ctx, vva.Address, delTokens, sdk.Unbonded, val1, true) @@ -158,6 +221,10 @@ func TestHandleVestingDebtForcedUnbond(t *testing.T) { _ = staking.EndBlocker(ctx, stakingKeeper) + vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address) + // t.Log(vva.GetDelegatedFree()) + t.Log(vva.GetDelegatedVesting()) + // require that there exists one delegation var delegations int stakingKeeper.IterateDelegations(ctx, vva.Address, func(index int64, d stakingexported.DelegationI) (stop bool) { @@ -173,12 +240,13 @@ func TestHandleVestingDebtForcedUnbond(t *testing.T) { ak.SetAccount(ctx, vva) keeper.UpdateVestedCoinsProgress(ctx, vva.Address, 0) vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address) + // require that period 0 coins have become debt require.Equal(t, sdk.NewCoins(sdk.NewInt64Coin(stakeDenom, 30000000)), vva.DebtAfterFailedVesting) // when there are no additional liquid coins in the account, require that there are no delegations after HandleVestingDebt (ie the account has been force unbonded) keeper.HandleVestingDebt(ctx, vva.Address, now.Add(12*time.Hour)) - // _ = staking.EndBlocker(ctx, stakingKeeper) + delegations = 0 stakingKeeper.IterateDelegations(ctx, vva.Address, func(index int64, d stakingexported.DelegationI) (stop bool) { delegations++ @@ -195,17 +263,14 @@ func TestHandleVestingDebtBurn(t *testing.T) { vva := ValidatorVestingDelegatorTestAccount(now) ak.SetAccount(ctx, vva) delTokens := sdk.TokensFromConsensusPower(30) - val1, found := stakingKeeper.GetValidator(ctx, valOpAddr1) + val1, found := stakingKeeper.GetValidator(ctx, ValOpAddr1) require.True(t, found) + // delegate half the tokens, which will make the period 1 coins that fail to vest immediately cover the debt. _, err := stakingKeeper.Delegate(ctx, vva.Address, delTokens, sdk.Unbonded, val1, true) require.NoError(t, err) _ = staking.EndBlocker(ctx, stakingKeeper) - // receive some coins so that the debt will be covered by liquid balance - recvAmt := sdk.Coins{sdk.NewInt64Coin(stakeDenom, 30000000)} - vva.SetCoins(vva.GetCoins().Add(recvAmt)) - // period 0 passes and the threshold is not met vva.MissingSignCount[0] = 50 vva.MissingSignCount[1] = 100 @@ -242,17 +307,13 @@ func TestHandleVestingDebtReturn(t *testing.T) { vva.ReturnAddress = TestAddrs[2] ak.SetAccount(ctx, vva) delTokens := sdk.TokensFromConsensusPower(30) - val1, found := stakingKeeper.GetValidator(ctx, valOpAddr1) + val1, found := stakingKeeper.GetValidator(ctx, ValOpAddr1) require.True(t, found) _, err := stakingKeeper.Delegate(ctx, vva.Address, delTokens, sdk.Unbonded, val1, true) require.NoError(t, err) _ = staking.EndBlocker(ctx, stakingKeeper) - // receive some coins so that the debt will be covered by liquid balance - recvAmt := sdk.Coins{sdk.NewInt64Coin(stakeDenom, 30000000)} - vva.SetCoins(vva.GetCoins().Add(recvAmt)) - // period 0 passes and the threshold is not met vva.MissingSignCount[0] = 50 vva.MissingSignCount[1] = 100 diff --git a/x/validator-vesting/internal/keeper/test_common.go b/x/validator-vesting/internal/keeper/test_common.go index a3ed9945..9930123e 100644 --- a/x/validator-vesting/internal/keeper/test_common.go +++ b/x/validator-vesting/internal/keeper/test_common.go @@ -37,22 +37,22 @@ var ( delAddr2 = sdk.AccAddress(delPk2.Address()) delAddr3 = sdk.AccAddress(delPk3.Address()) - valOpPk1 = ed25519.GenPrivKey().PubKey() - valOpPk2 = ed25519.GenPrivKey().PubKey() - valOpPk3 = ed25519.GenPrivKey().PubKey() - valOpAddr1 = sdk.ValAddress(valOpPk1.Address()) - valOpAddr2 = sdk.ValAddress(valOpPk2.Address()) - valOpAddr3 = sdk.ValAddress(valOpPk3.Address()) - valAccAddr1 = sdk.AccAddress(valOpPk1.Address()) // generate acc addresses for these validator keys too - valAccAddr2 = sdk.AccAddress(valOpPk2.Address()) - valAccAddr3 = sdk.AccAddress(valOpPk3.Address()) + ValOpPk1 = ed25519.GenPrivKey().PubKey() + ValOpPk2 = ed25519.GenPrivKey().PubKey() + ValOpPk3 = ed25519.GenPrivKey().PubKey() + ValOpAddr1 = sdk.ValAddress(ValOpPk1.Address()) + ValOpAddr2 = sdk.ValAddress(ValOpPk2.Address()) + ValOpAddr3 = sdk.ValAddress(ValOpPk3.Address()) + valAccAddr1 = sdk.AccAddress(ValOpPk1.Address()) // generate acc addresses for these validator keys too + valAccAddr2 = sdk.AccAddress(ValOpPk2.Address()) + valAccAddr3 = sdk.AccAddress(ValOpPk3.Address()) - valConsPk1 = ed25519.GenPrivKey().PubKey() - valConsPk2 = ed25519.GenPrivKey().PubKey() - valConsPk3 = ed25519.GenPrivKey().PubKey() - valConsAddr1 = sdk.ConsAddress(valConsPk1.Address()) - valConsAddr2 = sdk.ConsAddress(valConsPk2.Address()) - valConsAddr3 = sdk.ConsAddress(valConsPk3.Address()) + ValConsPk11 = ed25519.GenPrivKey().PubKey() + ValConsPk12 = ed25519.GenPrivKey().PubKey() + ValConsPk13 = ed25519.GenPrivKey().PubKey() + ValConsAddr1 = sdk.ConsAddress(ValConsPk11.Address()) + ValConsAddr2 = sdk.ConsAddress(ValConsPk12.Address()) + ValConsAddr3 = sdk.ConsAddress(ValConsPk13.Address()) // TODO move to common testing package for all modules // test addresses @@ -177,6 +177,32 @@ func ValidatorVestingTestAccount() *types.ValidatorVestingAccount { return vva } +func ValidatorVestingTestAccounts(numAccounts int) []*types.ValidatorVestingAccount { + now := tmtime.Now() + periods := vesting.VestingPeriods{ + vesting.VestingPeriod{PeriodLength: int64(12 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + } + testAddr := types.CreateTestAddrs(numAccounts) + testPk := types.CreateTestPubKeys(numAccounts) + var vvas []*types.ValidatorVestingAccount + for i := 0; i < numAccounts; i++ { + + testConsAddr := sdk.ConsAddress(testPk[i].Address()) + origCoins := sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)} + bacc := auth.NewBaseAccountWithAddress(testAddr[i]) + bacc.SetCoins(origCoins) + vva := types.NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90) + err := vva.Validate() + if err != nil { + panic(err) + } + vvas = append(vvas, vva) + } + return vvas +} + func ValidatorVestingDelegatorTestAccount(startTime time.Time) *types.ValidatorVestingAccount { periods := vesting.VestingPeriods{ vesting.VestingPeriod{PeriodLength: int64(12 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(stakeDenom, 30000000)}}, @@ -198,9 +224,9 @@ func ValidatorVestingDelegatorTestAccount(startTime time.Time) *types.ValidatorV } func CreateValidators(ctx sdk.Context, sk staking.Keeper, powers []int64) { - val1 := staking.NewValidator(valOpAddr1, valOpPk1, staking.Description{}) - val2 := staking.NewValidator(valOpAddr2, valOpPk2, staking.Description{}) - val3 := staking.NewValidator(valOpAddr3, valOpPk3, staking.Description{}) + val1 := staking.NewValidator(ValOpAddr1, ValOpPk1, staking.Description{}) + val2 := staking.NewValidator(ValOpAddr2, ValOpPk2, staking.Description{}) + val3 := staking.NewValidator(ValOpAddr3, ValOpPk3, staking.Description{}) sk.SetValidator(ctx, val1) sk.SetValidator(ctx, val2) diff --git a/x/validator-vesting/internal/types/genesis.go b/x/validator-vesting/internal/types/genesis.go index 60fd9a05..29013bd1 100644 --- a/x/validator-vesting/internal/types/genesis.go +++ b/x/validator-vesting/internal/types/genesis.go @@ -1,6 +1,8 @@ package types import ( + "bytes" + "github.com/cosmos/cosmos-sdk/x/auth/exported" ) @@ -20,3 +22,15 @@ func NewGenesisState(accounts exported.GenesisAccounts) GenesisState { func DefaultGenesisState() GenesisState { return NewGenesisState(exported.GenesisAccounts{}) } + +// Equal checks whether two gov GenesisState structs are equivalent +func (data GenesisState) Equal(data2 GenesisState) bool { + b1 := ModuleCdc.MustMarshalBinaryBare(data) + b2 := ModuleCdc.MustMarshalBinaryBare(data2) + return bytes.Equal(b1, b2) +} + +// IsEmpty returns true if a GenesisState is empty +func (data GenesisState) IsEmpty() bool { + return data.Equal(GenesisState{}) +} diff --git a/x/validator-vesting/internal/types/key.go b/x/validator-vesting/internal/types/key.go index a86c1aea..6b198440 100644 --- a/x/validator-vesting/internal/types/key.go +++ b/x/validator-vesting/internal/types/key.go @@ -13,11 +13,13 @@ const ( ) var ( + // BlocktimeKey key for the time of the previous block + BlocktimeKey = []byte{0x00} // ValidatorVestingAccountPrefix store prefix for validator vesting accounts - ValidatorVestingAccountPrefix = []byte{0x01} + ValidatorVestingAccountPrefix = []byte{0x01} ) // ValidatorVestingAccountKey returns the account address bytes prefixed by ValidatorVestingAccountPrefix func ValidatorVestingAccountKey(addr sdk.AccAddress) []byte { return append(ValidatorVestingAccountPrefix, addr.Bytes()...) -} \ No newline at end of file +} diff --git a/x/validator-vesting/internal/types/test_common.go b/x/validator-vesting/internal/types/test_common.go index 6e6091ba..b34c14b2 100644 --- a/x/validator-vesting/internal/types/test_common.go +++ b/x/validator-vesting/internal/types/test_common.go @@ -1,4 +1,8 @@ -package types // noalias +package types + +// nolint:deadcode unused +// DONTCOVER +// noalias import ( "bytes" diff --git a/x/validator-vesting/internal/types/validator_vesting_account.go b/x/validator-vesting/internal/types/validator_vesting_account.go index 00f326df..3aad1824 100644 --- a/x/validator-vesting/internal/types/validator_vesting_account.go +++ b/x/validator-vesting/internal/types/validator_vesting_account.go @@ -36,7 +36,7 @@ type ValidatorVestingAccount struct { ReturnAddress sdk.AccAddress `json:"return_address" yaml:"return_address"` SigningThreshold int64 `json:"signing_threshold" yaml:"signing_threshold"` MissingSignCount []int64 `json:"missing_sign_count" yaml:"missing_sign_count"` - VestingPeriodProgress []int `json:"vesting_period_progress" yaml:"vesting_period_progress"` + VestingPeriodProgress [][]int `json:"vesting_period_progress" yaml:"vesting_period_progress"` DebtAfterFailedVesting sdk.Coins `json:"debt_after_failed_vesting" yaml:"debt_after_failed_vesting"` } @@ -51,7 +51,10 @@ func NewValidatorVestingAccountRaw(bva *vesting.BaseVestingAccount, ContinuousVestingAccount: cva, VestingPeriods: periods, } - var vestingPeriodProgress = make([]int, len(periods)) + var vestingPeriodProgress = make([][]int, len(periods)) + for i := range vestingPeriodProgress { + vestingPeriodProgress[i] = make([]int, 2) + } return &ValidatorVestingAccount{ PeriodicVestingAccount: pva, @@ -84,7 +87,10 @@ func NewValidatorVestingAccount(baseAcc *auth.BaseAccount, startTime int64, peri ContinuousVestingAccount: cva, VestingPeriods: periods, } - var vestingPeriodProgress = make([]int, len(periods)) + var vestingPeriodProgress = make([][]int, len(periods)) + for i := range vestingPeriodProgress { + vestingPeriodProgress[i] = make([]int, 2) + } debt := sdk.NewCoins() @@ -110,8 +116,8 @@ func (vva ValidatorVestingAccount) GetVestedCoins(blockTime time.Time) sdk.Coins for i := 0; i < numberPeriods; i++ { x := blockTime.Unix() - currentPeriodStartTime if x >= vva.VestingPeriods[i].PeriodLength { - vestedSuccess := vva.VestingPeriodProgress[i] > 0 - if vestedSuccess { + vestingComplete := vva.VestingPeriodProgress[i][0] == 1 + if vestingComplete { vestedCoins = vestedCoins.Add(vva.VestingPeriods[i].VestingAmount) } currentPeriodStartTime += vva.VestingPeriods[i].PeriodLength @@ -124,21 +130,15 @@ func (vva ValidatorVestingAccount) GetVestedCoins(blockTime time.Time) sdk.Coins } // GetFailedVestedCoins returns the total number of coins for which the vesting period has passed but the vesting threshold was not met. -func (vva ValidatorVestingAccount) GetFailedVestedCoins(blockTime time.Time) sdk.Coins { +func (vva ValidatorVestingAccount) GetFailedVestedCoins() sdk.Coins { var failedVestedCoins sdk.Coins - if blockTime.Unix() <= vva.StartTime { - return failedVestedCoins - } - currentPeriodStartTime := vva.StartTime numberPeriods := len(vva.VestingPeriods) for i := 0; i < numberPeriods; i++ { - x := blockTime.Unix() - currentPeriodStartTime - if x >= vva.VestingPeriods[i].PeriodLength { - vestedFailure := vva.VestingPeriodProgress[i] == 0 + if vva.VestingPeriodProgress[i][0] == 1 { + vestedFailure := vva.VestingPeriodProgress[i][1] == 0 if vestedFailure { failedVestedCoins = failedVestedCoins.Add(vva.VestingPeriods[i].VestingAmount) } - currentPeriodStartTime += vva.VestingPeriods[i].PeriodLength } else { break } @@ -148,13 +148,13 @@ func (vva ValidatorVestingAccount) GetFailedVestedCoins(blockTime time.Time) sdk // GetVestingCoins returns the total number of vesting coins. For validator vesting accounts, this excludes coins for which the vesting period has passed, but the vesting threshold was not met. func (vva ValidatorVestingAccount) GetVestingCoins(blockTime time.Time) sdk.Coins { - return vva.OriginalVesting.Sub(vva.GetVestedCoins(blockTime)).Sub(vva.GetFailedVestedCoins(blockTime)) + return vva.OriginalVesting.Sub(vva.GetVestedCoins(blockTime)) } // SpendableCoins returns the total number of spendable coins per denom for a // periodic vesting account. func (vva ValidatorVestingAccount) SpendableCoins(blockTime time.Time) sdk.Coins { - return vva.BaseVestingAccount.SpendableCoinsFromVestingCoins(vva.GetVestingCoins(blockTime)).Sub(vva.DebtAfterFailedVesting) + return vva.BaseVestingAccount.SpendableCoinsFromVestingCoins(vva.GetVestingCoins(blockTime)) } // TrackDelegation tracks a desired delegation amount by setting the appropriate @@ -204,7 +204,7 @@ func (vva ValidatorVestingAccount) MarshalYAML() (interface{}, error) { ReturnAddress sdk.AccAddress SigningThreshold int64 MissingSignCount []int64 - VestingPeriodProgress []int + VestingPeriodProgress [][]int DebtAfterFailedVesting sdk.Coins }{ Address: vva.Address, diff --git a/x/validator-vesting/internal/types/validator_vesting_account_test.go b/x/validator-vesting/internal/types/validator_vesting_account_test.go index e3e81dd8..5401bd4f 100644 --- a/x/validator-vesting/internal/types/validator_vesting_account_test.go +++ b/x/validator-vesting/internal/types/validator_vesting_account_test.go @@ -19,6 +19,29 @@ var ( feeDenom = "fee" ) +func TestNewAccount(t *testing.T) { + now := tmtime.Now() + endTime := now.Add(24 * time.Hour).Unix() + periods := vesting.VestingPeriods{ + vesting.VestingPeriod{PeriodLength: int64(12 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + } + + testAddr := CreateTestAddrs(1)[0] + testPk := CreateTestPubKeys(1)[0] + testConsAddr := sdk.ConsAddress(testPk.Address()) + origCoins := sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)} + bacc := auth.NewBaseAccountWithAddress(testAddr) + bacc.SetCoins(origCoins) + bva := vesting.NewBaseVestingAccount(&bacc, origCoins, endTime) + require.NotPanics(t, func() { NewValidatorVestingAccountRaw(bva, now.Unix(), periods, testConsAddr, nil, 90) }) + vva := NewValidatorVestingAccountRaw(bva, now.Unix(), periods, testConsAddr, nil, 90) + vva.PubKey = testPk + _, err := vva.MarshalYAML() + require.NoError(t, err) +} + func TestGetVestedCoinsValidatorVestingAcc(t *testing.T) { now := tmtime.Now() periods := vesting.VestingPeriods{ @@ -44,40 +67,51 @@ func TestGetVestedCoinsValidatorVestingAcc(t *testing.T) { require.Nil(t, vestedCoins) // require 50% of coins vested after successful period 1 vesting - vva.VestingPeriodProgress[0] = 1 + vva.VestingPeriodProgress[0] = []int{1, 1} vestedCoins = vva.GetVestedCoins(now.Add(12 * time.Hour)) require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestedCoins) - // require no coins vested after unsuccessful period 1 vesting - vva.VestingPeriodProgress[0] = 0 + // require 50% of coins vested after unsuccessful period 1 vesting + // NOTE: There is a fairly important semantic distinction here. It seems tempting to say that a failed vesting period should mean that 'GetVestedCoins' should not return those coins. While the point of a validator vesting account is to 'seize' or 'burn' unsuccessfully vested coins, they do in fact vest and become spendable. The intuition is that they have to be spendable in order for the bank keeper to allow us to send/burn them. If they were not vested, then a validator vesting account that failed all of it's vesting periods would never return/burn the coins because it would never have a spendable balance by which to do so. They way we prevent them from being spent in a way other than return/burn is by sending them in the BeginBlock and thus beating any other transfers that would otherwise occur. + vva.VestingPeriodProgress[0] = []int{1, 0} vestedCoins = vva.GetVestedCoins(now.Add(12 * time.Hour)) - require.Nil(t, vestedCoins) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestedCoins) // require period 2 coins don't vest until period is over - vva.VestingPeriodProgress[0] = 1 + vva.VestingPeriodProgress[0] = []int{1, 1} // even if the vesting period was somehow successful, should still only return 50% of coins as vested, since the second vesting period hasn't completed. - vva.VestingPeriodProgress[1] = 1 + vva.VestingPeriodProgress[1] = []int{1, 1} vestedCoins = vva.GetVestedCoins(now.Add(15 * time.Hour)) require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestedCoins) // require 75% of coins vested after successful period 2 - vva.VestingPeriodProgress[0] = 1 - vva.VestingPeriodProgress[1] = 1 + vva.VestingPeriodProgress[0] = []int{1, 1} + vva.VestingPeriodProgress[1] = []int{1, 1} vestedCoins = vva.GetVestedCoins(now.Add(18 * time.Hour)) require.Equal(t, sdk.Coins{ sdk.NewInt64Coin(feeDenom, 750), sdk.NewInt64Coin(stakeDenom, 75)}, vestedCoins) - // require 50% of coins vested after successful period 1 and unsuccessful period 2 - vva.VestingPeriodProgress[0] = 1 - vva.VestingPeriodProgress[1] = 0 + // require 75% of coins vested after successful period 1 and unsuccessful period 2. + vva.VestingPeriodProgress[0] = []int{1, 1} + vva.VestingPeriodProgress[1] = []int{1, 0} vestedCoins = vva.GetVestedCoins(now.Add(18 * time.Hour)) - require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestedCoins) + require.Equal(t, + sdk.Coins{ + sdk.NewInt64Coin(feeDenom, 750), sdk.NewInt64Coin(stakeDenom, 75)}, vestedCoins) // require 100% of coins vested after all periods complete successfully - vva.VestingPeriodProgress[0] = 1 - vva.VestingPeriodProgress[1] = 1 - vva.VestingPeriodProgress[2] = 1 + vva.VestingPeriodProgress[0] = []int{1, 1} + vva.VestingPeriodProgress[1] = []int{1, 1} + vva.VestingPeriodProgress[2] = []int{1, 1} + + vestedCoins = vva.GetVestedCoins(now.Add(48 * time.Hour)) + require.Equal(t, origCoins, vestedCoins) + + // require 100% of coins vested after all periods complete unsuccessfully + vva.VestingPeriodProgress[0] = []int{1, 0} + vva.VestingPeriodProgress[1] = []int{1, 0} + vva.VestingPeriodProgress[2] = []int{1, 0} vestedCoins = vva.GetVestedCoins(now.Add(48 * time.Hour)) require.Equal(t, origCoins, vestedCoins) @@ -108,48 +142,48 @@ func TestGetVestingCoinsValidatorVestingAcc(t *testing.T) { require.Equal(t, origCoins, vestingCoins) // require 50% of coins vesting after successful period 1 vesting - vva.VestingPeriodProgress[0] = 1 + vva.VestingPeriodProgress[0] = []int{1, 1} vestingCoins = vva.GetVestingCoins(now.Add(12 * time.Hour)) require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestingCoins) // require 50% of coins vesting after unsuccessful period 1 vesting - vva.VestingPeriodProgress[0] = 0 + vva.VestingPeriodProgress[0] = []int{1, 0} vestingCoins = vva.GetVestingCoins(now.Add(12 * time.Hour)) require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestingCoins) // require period 2 coins still vesting until period is over - vva.VestingPeriodProgress[0] = 1 + vva.VestingPeriodProgress[0] = []int{1, 1} // should never happen, but still won't affect vesting balance - vva.VestingPeriodProgress[1] = 1 + vva.VestingPeriodProgress[1] = []int{1, 1} vestingCoins = vva.GetVestingCoins(now.Add(15 * time.Hour)) require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestingCoins) // require 25% of coins vesting after successful period 2 - vva.VestingPeriodProgress[0] = 1 - vva.VestingPeriodProgress[1] = 1 + vva.VestingPeriodProgress[0] = []int{1, 1} + vva.VestingPeriodProgress[1] = []int{1, 1} vestingCoins = vva.GetVestingCoins(now.Add(18 * time.Hour)) require.Equal(t, sdk.Coins{ sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}, vestingCoins) // require 25% of coins vesting after successful period 1 and unsuccessful period 2 - vva.VestingPeriodProgress[0] = 1 - vva.VestingPeriodProgress[1] = 0 + vva.VestingPeriodProgress[0] = []int{1, 1} + vva.VestingPeriodProgress[1] = []int{1, 1} vestingCoins = vva.GetVestingCoins(now.Add(18 * time.Hour)) require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}, vestingCoins) // require no coins vesting after all periods complete successfully - vva.VestingPeriodProgress[0] = 1 - vva.VestingPeriodProgress[1] = 1 - vva.VestingPeriodProgress[2] = 1 + vva.VestingPeriodProgress[0] = []int{1, 1} + vva.VestingPeriodProgress[1] = []int{1, 1} + vva.VestingPeriodProgress[2] = []int{1, 1} vestingCoins = vva.GetVestingCoins(now.Add(48 * time.Hour)) require.Nil(t, vestingCoins) // require no coins vesting after all periods complete unsuccessfully - vva.VestingPeriodProgress[0] = 0 - vva.VestingPeriodProgress[1] = 0 - vva.VestingPeriodProgress[2] = 0 + vva.VestingPeriodProgress[0] = []int{1, 0} + vva.VestingPeriodProgress[1] = []int{1, 0} + vva.VestingPeriodProgress[2] = []int{1, 0} vestingCoins = vva.GetVestingCoins(now.Add(48 * time.Hour)) require.Nil(t, vestingCoins) @@ -176,21 +210,21 @@ func TestSpendableCoinsValidatorVestingAccount(t *testing.T) { require.Nil(t, spendableCoins) // require that all vested coins (50%) are spendable when period 1 completes successfully - vva.VestingPeriodProgress[0] = 1 + vva.VestingPeriodProgress[0] = []int{1, 1} spendableCoins = vva.SpendableCoins(now.Add(12 * time.Hour)) require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, spendableCoins) - // require that there exist no spendable coins after period 1 completes unsuccessfully. - vva.VestingPeriodProgress[0] = 0 - spendableCoins = vva.SpendableCoins(now) - require.Nil(t, spendableCoins) + // require that 50% of coins are spendable after period 1 completes unsuccessfully. See note above. The reason the coins are still 'spendable' is that we need to be able to transfer the coins to the return address/burn them. Making them not spendable means that it would be impossible to recover the debt for a validator vesting account for which all periods failed. + vva.VestingPeriodProgress[0] = []int{1, 0} + spendableCoins = vva.SpendableCoins(now.Add(12 * time.Hour)) + require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, spendableCoins) // receive some coins recvAmt := sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)} vva.SetCoins(vva.GetCoins().Add(recvAmt)) - // require that all vested coins (50%) are spendable plus any received after period 1 completes unsuccessfully - vva.VestingPeriodProgress[0] = 1 + // require that all vested coins (50%) are spendable plus any received after period 1 completes successfully + vva.VestingPeriodProgress[0] = []int{1, 1} spendableCoins = vva.SpendableCoins(now.Add(12 * time.Hour)) require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 100)}, spendableCoins) @@ -202,6 +236,36 @@ func TestSpendableCoinsValidatorVestingAccount(t *testing.T) { require.Nil(t, spendableCoins) } +func TestGetFailedVestedCoins(t *testing.T) { + now := tmtime.Now() + periods := vesting.VestingPeriods{ + vesting.VestingPeriod{PeriodLength: int64(12 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(feeDenom, 250), sdk.NewInt64Coin(stakeDenom, 25)}}, + } + + testAddr := CreateTestAddrs(1)[0] + testPk := CreateTestPubKeys(1)[0] + testConsAddr := sdk.ConsAddress(testPk.Address()) + origCoins := sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)} + bacc := auth.NewBaseAccountWithAddress(testAddr) + bacc.SetCoins(origCoins) + vva := NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90) + + vva.VestingPeriodProgress[0] = []int{1, 0} + // require that period 1 coins are failed if the period completed unsucessfully. + require.Equal(t, + sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, + vva.GetFailedVestedCoins(), + ) + + vva.VestingPeriodProgress[0] = []int{1, 1} + require.Equal(t, + sdk.Coins(nil), + vva.GetFailedVestedCoins(), + ) + +} func TestTrackDelegationValidatorVestingAcc(t *testing.T) { now := tmtime.Now() periods := vesting.VestingPeriods{ @@ -222,6 +286,20 @@ func TestTrackDelegationValidatorVestingAcc(t *testing.T) { require.Equal(t, origCoins, vva.DelegatedVesting) require.Nil(t, vva.DelegatedFree) require.Nil(t, vva.GetCoins()) + require.Nil(t, vva.SpendableCoins(now)) + + // all periods pass successfully + bacc.SetCoins(origCoins) + vva = NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90) + vva.VestingPeriodProgress[0] = []int{1, 1} + vva.VestingPeriodProgress[1] = []int{1, 1} + vva.VestingPeriodProgress[2] = []int{1, 1} + vva.TrackDelegation(now.Add(48*time.Hour), origCoins) + // require all delegated coins are free + require.Equal(t, origCoins, vva.DelegatedFree) + require.Nil(t, vva.DelegatedVesting) + require.Nil(t, vva.GetCoins()) + require.Nil(t, vva.SpendableCoins(now.Add(48*time.Hour))) // require the ability to delegate all vesting coins (50%) and all vested coins (50%) bacc.SetCoins(origCoins) @@ -230,7 +308,7 @@ func TestTrackDelegationValidatorVestingAcc(t *testing.T) { require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, vva.DelegatedVesting) require.Nil(t, vva.DelegatedFree) - vva.VestingPeriodProgress[0] = 1 + vva.VestingPeriodProgress[0] = []int{1, 1} vva.TrackDelegation(now.Add(12*time.Hour), sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, vva.DelegatedVesting) require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, vva.DelegatedFree) @@ -273,9 +351,9 @@ func TestTrackUndelegationPeriodicVestingAcc(t *testing.T) { // require the ability to delegate all coins after they have successfully vested bacc.SetCoins(origCoins) vva = NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90) - vva.VestingPeriodProgress[0] = 1 - vva.VestingPeriodProgress[1] = 1 - vva.VestingPeriodProgress[2] = 1 + vva.VestingPeriodProgress[0] = []int{1, 1} + vva.VestingPeriodProgress[1] = []int{1, 1} + vva.VestingPeriodProgress[2] = []int{1, 1} vva.TrackDelegation(now.Add(24*time.Hour), origCoins) vva.TrackUndelegation(origCoins) require.Nil(t, vva.DelegatedFree) @@ -294,7 +372,7 @@ func TestTrackUndelegationPeriodicVestingAcc(t *testing.T) { // successfuly vest period 1 and delegate to two validators vva = NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90) - vva.VestingPeriodProgress[0] = 1 + vva.VestingPeriodProgress[0] = []int{1, 1} vva.TrackDelegation(now.Add(12*time.Hour), sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) vva.TrackDelegation(now.Add(12*time.Hour), sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}) diff --git a/x/validator-vesting/test_common.go b/x/validator-vesting/test_common.go new file mode 100644 index 00000000..c6b1d40b --- /dev/null +++ b/x/validator-vesting/test_common.go @@ -0,0 +1,133 @@ +package validatorvesting + +// nolint +// DONTCOVER + +import ( + "testing" + + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" + + 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/mock" + "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/cosmos/cosmos-sdk/x/supply" + supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported" + "github.com/cosmos/cosmos-sdk/x/validator-vesting/internal/keeper" + "github.com/cosmos/cosmos-sdk/x/validator-vesting/internal/types" +) + +var ( + valTokens = sdk.TokensFromConsensusPower(42) + initTokens = sdk.TokensFromConsensusPower(100000) + valCoins = sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, valTokens)) + initCoins = sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, initTokens)) +) + +type testInput struct { + mApp *mock.App + keeper keeper.Keeper + sk staking.Keeper + addrs []sdk.AccAddress + pubKeys []crypto.PubKey + privKeys []crypto.PrivKey +} + +func getMockApp(t *testing.T, numGenAccs int, genState types.GenesisState, genAccs []auth.Account) testInput { + mApp := mock.NewApp() + + staking.RegisterCodec(mApp.Cdc) + types.RegisterCodec(mApp.Cdc) + supply.RegisterCodec(mApp.Cdc) + + keyStaking := sdk.NewKVStoreKey(staking.StoreKey) + keyValidatorVesting := sdk.NewKVStoreKey(types.StoreKey) + keySupply := sdk.NewKVStoreKey(supply.StoreKey) + + validatorVestingAcc := supply.NewEmptyModuleAccount(types.ModuleName, supply.Burner) + notBondedPool := supply.NewEmptyModuleAccount(staking.NotBondedPoolName, supply.Burner, supply.Staking) + bondPool := supply.NewEmptyModuleAccount(staking.BondedPoolName, supply.Burner, supply.Staking) + + blacklistedAddrs := make(map[string]bool) + blacklistedAddrs[validatorVestingAcc.GetAddress().String()] = true + blacklistedAddrs[notBondedPool.GetAddress().String()] = true + blacklistedAddrs[bondPool.GetAddress().String()] = true + + pk := mApp.ParamsKeeper + + bk := bank.NewBaseKeeper(mApp.AccountKeeper, mApp.ParamsKeeper.Subspace(bank.DefaultParamspace), bank.DefaultCodespace, blacklistedAddrs) + + maccPerms := map[string][]string{ + types.ModuleName: {supply.Burner}, + staking.NotBondedPoolName: {supply.Burner, supply.Staking}, + staking.BondedPoolName: {supply.Burner, supply.Staking}, + } + supplyKeeper := supply.NewKeeper(mApp.Cdc, keySupply, mApp.AccountKeeper, bk, maccPerms) + sk := staking.NewKeeper( + mApp.Cdc, keyStaking, supplyKeeper, pk.Subspace(staking.DefaultParamspace), staking.DefaultCodespace, + ) + + keeper := keeper.NewKeeper( + mApp.Cdc, keyValidatorVesting, mApp.AccountKeeper, bk, supplyKeeper, sk) + + mApp.SetBeginBlocker(getBeginBlocker(keeper)) + mApp.SetInitChainer(getInitChainer(mApp, keeper, sk, supplyKeeper, genAccs, genState, + []supplyexported.ModuleAccountI{validatorVestingAcc, notBondedPool, bondPool})) + + require.NoError(t, mApp.CompleteSetup(keyStaking, keyValidatorVesting, keySupply)) + + var ( + addrs []sdk.AccAddress + pubKeys []crypto.PubKey + privKeys []crypto.PrivKey + ) + + if genAccs == nil || len(genAccs) == 0 { + genAccs, addrs, pubKeys, privKeys = mock.CreateGenAccounts(numGenAccs, valCoins) + } + + mock.SetGenesis(mApp, genAccs) + + return testInput{mApp, keeper, sk, addrs, pubKeys, privKeys} +} + +// gov and staking endblocker +func getBeginBlocker(keeper Keeper) sdk.BeginBlocker { + return func(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { + BeginBlocker(ctx, req, keeper) + return abci.ResponseBeginBlock{} + } +} + +// gov and staking initchainer +func getInitChainer(mapp *mock.App, keeper Keeper, stakingKeeper staking.Keeper, supplyKeeper supply.Keeper, accs []auth.Account, genState GenesisState, + blacklistedAddrs []supplyexported.ModuleAccountI) sdk.InitChainer { + return func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { + mapp.InitChainer(ctx, req) + + stakingGenesis := staking.DefaultGenesisState() + + totalSupply := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, initTokens.MulRaw(int64(len(mapp.GenesisAccounts))))) + supplyKeeper.SetSupply(ctx, supply.NewSupply(totalSupply)) + + // set module accounts + for _, macc := range blacklistedAddrs { + supplyKeeper.SetModuleAccount(ctx, macc) + } + + validators := staking.InitGenesis(ctx, stakingKeeper, mapp.AccountKeeper, supplyKeeper, stakingGenesis) + if genState.IsEmpty() { + InitGenesis(ctx, keeper, mapp.AccountKeeper, types.DefaultGenesisState()) + } else { + InitGenesis(ctx, keeper, mapp.AccountKeeper, genState) + } + return abci.ResponseInitChain{ + Validators: validators, + } + } +}