feat: validator vesting accounts

This commit is contained in:
Kevin Davis 2019-09-23 14:23:00 -04:00
parent 3871e5a84b
commit 918a43e7ab
13 changed files with 1579 additions and 0 deletions

View File

@ -0,0 +1,73 @@
package validatorvesting
import (
"bytes"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/validator-vesting/internal/keeper"
abci "github.com/tendermint/tendermint/abci/types"
)
// BeginBlocker updates the vote signing information for each validator vesting account, updates account when period changes, and updates the previousBlockTime value in the store.
func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k keeper.Keeper) {
previousBlockTime := time.Time{}
if ctx.BlockHeight() > 1 {
previousBlockTime = k.GetPreviousBlockTime(ctx)
}
currentBlockTime := req.Header.GetTime()
var voteInfos VoteInfos
voteInfos = ctx.VoteInfos()
validatorVestingKeys := k.GetAllAccountKeys(ctx)
for _, key := range validatorVestingKeys {
acc := k.GetAccountFromAuthKeeper(ctx, key)
if voteInfos.ContainsValidatorAddress(acc.ValidatorAddress) {
vote := voteInfos.MustFilterByValidatorAddress(acc.ValidatorAddress)
if !vote.SignedLastBlock {
// if the validator explicitly missed signing the block, increment the missing sign count
k.UpdateMissingSignCount(ctx, acc.GetAddress(), true)
} else {
k.UpdateMissingSignCount(ctx, acc.GetAddress(), false)
}
} else {
// if the validator was not a voting member of the validator set, increment the missing sign count
k.UpdateMissingSignCount(ctx, acc.GetAddress(), true)
}
// check if a period ended in the last block
endTimes := k.GetPeriodEndTimes(ctx, key)
for i, t := range endTimes {
if currentBlockTime.Unix() >= t && previousBlockTime.Unix() < t {
k.UpdateVestedCoinsProgress(ctx, key, i)
}
k.HandleVestingDebt(ctx, key, currentBlockTime)
}
}
k.SetPreviousBlockTime(ctx, currentBlockTime)
}
// VoteInfos an array of abci.VoteInfo
type VoteInfos []abci.VoteInfo
// ContainsValidatorAddress returns true if the input validator address is found in the VoteInfos array
func (vis VoteInfos) ContainsValidatorAddress(consAddress sdk.ConsAddress) bool {
for _, vi := range vis {
votingAddress := sdk.ConsAddress(vi.Validator.Address)
if bytes.Equal(consAddress, votingAddress) {
return true
}
}
return false
}
// MustFilterByValidatorAddress returns the VoteInfo that has a validator address matching the input validator address
func (vis VoteInfos) MustFilterByValidatorAddress(consAddress sdk.ConsAddress) abci.VoteInfo {
for i, vi := range vis {
votingAddress := sdk.ConsAddress(vi.Validator.Address)
if bytes.Equal(consAddress, votingAddress) {
return vis[i]
}
}
panic("validator address not found")
}

View File

@ -0,0 +1,22 @@
// nolint
package validatorvesting
import (
"github.com/cosmos/cosmos-sdk/x/validator-vesting/internal/keeper"
"github.com/cosmos/cosmos-sdk/x/validator-vesting/internal/types"
)
const (
ModuleName = types.ModuleName
StoreKey = types.StoreKey
)
var (
NewGenesisState = types.NewGenesisState
)
type (
GenesisState = types.GenesisState
Keeper = keeper.Keeper
ValidatorVestingAccount = types.ValidatorVestingAccount
)

View File

@ -0,0 +1,19 @@
package validatorvesting
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/validator-vesting/internal/types"
)
// 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
func InitGenesis(ctx sdk.Context, keeper Keeper, accountKeeper types.AccountKeeper, data GenesisState) {
data.Accounts = auth.SanitizeGenesisAccounts(data.Accounts)
for _, a := range data.Accounts {
vv, ok := a.(ValidatorVestingAccount)
if ok {
keeper.SetValidatorVestingAccountKey(ctx, vv.Address)
}
}
}

View File

@ -0,0 +1,188 @@
package keeper
import (
"fmt"
"time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported"
"github.com/cosmos/cosmos-sdk/x/validator-vesting/internal/types"
"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
cdc *codec.Codec
ak types.AccountKeeper
bk types.BankKeeper
supplyKeeper types.SupplyKeeper
stakingKeeper types.StakingKeeper
}
// NewKeeper creates a new Keeper instance
func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, ak types.AccountKeeper, bk types.BankKeeper, sk types.SupplyKeeper, stk types.StakingKeeper) Keeper {
return Keeper{
cdc: cdc,
storeKey: key,
ak: ak,
bk: bk,
supplyKeeper: sk,
stakingKeeper: stk,
}
}
// Logger returns a module-specific logger.
func (k Keeper) Logger(ctx sdk.Context) log.Logger {
return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName))
}
// 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)
if b == nil {
panic("Previous block time not set")
}
k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &blockTime)
return blockTime
}
// SetPreviousBlockTime set the time of the previous block
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
})
}
}
}
// 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.
func (k Keeper) SetValidatorVestingAccountKey(ctx sdk.Context, addr sdk.AccAddress) {
store := ctx.KVStore(k.storeKey)
// using empty bytes as value since the only thing we want to do is iterate over the keys.
store.Set(types.ValidatorVestingAccountKey(addr), []byte{0})
}
// IterateAccountKeys iterates over all the stored account keys and performs a callback function
func (k Keeper) IterateAccountKeys(ctx sdk.Context, cb func(accountKey []byte) (stop bool)) {
store := ctx.KVStore(k.storeKey)
iterator := sdk.KVStorePrefixIterator(store, types.ValidatorVestingAccountPrefix)
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
accountKey := iterator.Key()
if cb(accountKey) {
break
}
}
}
// GetAllAccountKeys returns all account keys in the validator vesting keeper.
func (k Keeper) GetAllAccountKeys(ctx sdk.Context) (keys [][]byte) {
k.IterateAccountKeys(ctx,
func(key []byte) (stop bool) {
keys = append(keys, key)
return false
})
return keys
}
// GetAccountFromAuthKeeper returns a ValidatorVestingAccount from the auth keeper
func (k Keeper) GetAccountFromAuthKeeper(ctx sdk.Context, addr sdk.AccAddress) *types.ValidatorVestingAccount {
acc := k.ak.GetAccount(ctx, addr)
vv, ok := acc.(*types.ValidatorVestingAccount)
if ok {
return vv
}
panic("validator vesting account not found")
}
// 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
vv := k.GetAccountFromAuthKeeper(ctx, addr)
currentEndTime := vv.StartTime
for _, p := range vv.VestingPeriods {
currentEndTime += p.PeriodLength
endTimes = append(endTimes, currentEndTime)
}
return endTimes
}

View File

@ -0,0 +1,282 @@
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 (
"testing"
"time"
"github.com/stretchr/testify/require"
tmtime "github.com/tendermint/tendermint/types/time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/staking"
stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported"
)
func TestGetSetValidatorVestingAccounts(t *testing.T) {
ctx, ak, _, _, _, keeper := CreateTestInput(t, false, 1000)
vva := ValidatorVestingTestAccount()
// Add the validator vesting account to the auth store
ak.SetAccount(ctx, vva)
// require that the keeper can set the account key without panic
require.NotPanics(t, func() { keeper.SetValidatorVestingAccountKey(ctx, vva.Address) })
// require that we can get the account from auth keeper as a validator vesting account.
require.NotPanics(t, func() { keeper.GetAccountFromAuthKeeper(ctx, vva.Address) })
// fetching a regular account from the auth keeper does not panic
require.NotPanics(t, func() { ak.GetAccount(ctx, TestAddrs[0]) })
// fetching a regular account from the validator vesting keeper panics.
require.Panics(t, func() { keeper.GetAccountFromAuthKeeper(ctx, TestAddrs[0]) })
// require that GetAllAccountKeys returns one account
keys := keeper.GetAllAccountKeys(ctx)
require.Equal(t, 1, len(keys))
}
func TestGetSetPreviousBlock(t *testing.T) {
ctx, _, _, _, _, keeper := CreateTestInput(t, false, 1000)
now := tmtime.Now()
// require panic if the previous blocktime was never set
require.Panics(t, func() { keeper.GetPreviousBlockTime(ctx) })
// require that passing a valid time to SetPreviousBlockTime does not panic
require.NotPanics(t, func() { keeper.SetPreviousBlockTime(ctx, now) })
// require that the value from GetPreviousBlockTime equals what was set
bpt := keeper.GetPreviousBlockTime(ctx)
require.Equal(t, now, bpt)
}
func TestSetMissingSignCount(t *testing.T) {
ctx, ak, _, _, _, keeper := CreateTestInput(t, false, 1000)
vva := ValidatorVestingTestAccount()
// Add the validator vesting account to the auth store
ak.SetAccount(ctx, vva)
// require empty array after ValidatorVestingAccount is initialized
require.Equal(t, []int64{0, 0}, vva.MissingSignCount)
// validator signs a block
keeper.UpdateMissingSignCount(ctx, vva.Address, false)
vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address)
require.Equal(t, []int64{0, 1}, vva.MissingSignCount)
// validator misses a block
keeper.UpdateMissingSignCount(ctx, vva.Address, true)
vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address)
require.Equal(t, []int64{1, 2}, vva.MissingSignCount)
}
func TestUpdateVestedCoinsProgress(t *testing.T) {
ctx, ak, _, _, _, keeper := CreateTestInput(t, false, 1000)
vva := ValidatorVestingTestAccount()
// Add the validator vesting account to the auth store
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)
// period 0 passes with all blocks signed
vva.MissingSignCount[0] = 0
vva.MissingSignCount[1] = 100
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, 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
vva.MissingSignCount[1] = 100
ak.SetAccount(ctx, vva)
keeper.UpdateVestedCoinsProgress(ctx, vva.Address, 0)
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 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)
vva := ValidatorVestingTestAccount()
// Delegate all coins
origCoins := sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 100)}
now := tmtime.Now()
vva.TrackDelegation(now, origCoins)
// Add the validator vesting account to the auth store
ak.SetAccount(ctx, vva)
// Require that calling HandleVestingDebt when debt is zero doesn't alter the delegation
keeper.HandleVestingDebt(ctx, vva.Address, now)
vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address)
require.Equal(t, origCoins, vva.DelegatedVesting)
require.Nil(t, vva.DelegatedFree)
require.Nil(t, vva.GetCoins())
// Create validators and a delegation from the validator vesting account
CreateValidators(ctx, stakingKeeper, []int64{5, 5, 5})
vva = ValidatorVestingDelegatorTestAccount(now)
ak.SetAccount(ctx, vva)
delTokens := sdk.TokensFromConsensusPower(30)
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)
// 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)
// period 0 passes and the threshold is not met
vva.MissingSignCount[0] = 50
vva.MissingSignCount[1] = 100
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++
return false
})
require.Equal(t, 0, delegations)
}
func TestHandleVestingDebtBurn(t *testing.T) {
ctx, ak, _, stakingKeeper, supplyKeeper, keeper := CreateTestInput(t, false, 1000)
CreateValidators(ctx, stakingKeeper, []int64{5, 5, 5})
now := tmtime.Now()
vva := ValidatorVestingDelegatorTestAccount(now)
ak.SetAccount(ctx, vva)
delTokens := sdk.TokensFromConsensusPower(30)
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
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)
initialSupply := supplyKeeper.GetSupply(ctx).GetTotal()
expectedSupply := initialSupply.Sub(vva.DebtAfterFailedVesting)
// Context needs the block time because bank keeper calls 'SpendableCoins' by getting the header from the context.
ctx = ctx.WithBlockTime(now.Add(12 * time.Hour))
keeper.HandleVestingDebt(ctx, vva.Address, now.Add(12*time.Hour))
// in the case when the return address is not set require that the total supply has decreased by the debt amount
require.Equal(t, expectedSupply, supplyKeeper.GetSupply(ctx).GetTotal())
// require that there is still one delegation
delegations := 0
stakingKeeper.IterateDelegations(ctx, vva.Address, func(index int64, d stakingexported.DelegationI) (stop bool) {
delegations++
return false
})
require.Equal(t, 1, delegations)
vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address)
//require that debt is now zero
require.Equal(t, sdk.Coins(nil), vva.DebtAfterFailedVesting)
}
func TestHandleVestingDebtReturn(t *testing.T) {
ctx, ak, _, stakingKeeper, _, keeper := CreateTestInput(t, false, 1000)
CreateValidators(ctx, stakingKeeper, []int64{5, 5, 5})
now := tmtime.Now()
vva := ValidatorVestingDelegatorTestAccount(now)
vva.ReturnAddress = TestAddrs[2]
ak.SetAccount(ctx, vva)
delTokens := sdk.TokensFromConsensusPower(30)
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
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)
initialBalance := ak.GetAccount(ctx, TestAddrs[2]).GetCoins()
expectedBalance := initialBalance.Add(vva.DebtAfterFailedVesting)
// Context needs the block time because bank keeper calls 'SpendableCoins' by getting the header from the context.
ctx = ctx.WithBlockTime(now.Add(12 * time.Hour))
keeper.HandleVestingDebt(ctx, vva.Address, now.Add(12*time.Hour))
// in the case when the return address is, set require that return address balance has increased by the debt amount
require.Equal(t, expectedBalance, ak.GetAccount(ctx, TestAddrs[2]).GetCoins())
// require that there is still one delegation
delegations := 0
stakingKeeper.IterateDelegations(ctx, vva.Address, func(index int64, d stakingexported.DelegationI) (stop bool) {
delegations++
return false
})
require.Equal(t, 1, delegations)
vva = keeper.GetAccountFromAuthKeeper(ctx, vva.Address)
//require that debt is now zero
require.Equal(t, sdk.Coins(nil), vva.DebtAfterFailedVesting)
}

View File

@ -0,0 +1,220 @@
package keeper
// nolint:deadcode unused
// DONTCOVER
// noalias
import (
"testing"
"time"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/libs/log"
tmtime "github.com/tendermint/tendermint/types/time"
dbm "github.com/tendermint/tm-db"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/cosmos-sdk/x/params"
"github.com/cosmos/cosmos-sdk/x/staking"
"github.com/cosmos/cosmos-sdk/x/supply"
"github.com/cosmos/cosmos-sdk/x/validator-vesting/internal/types"
)
//nolint: deadcode unused
var (
delPk1 = ed25519.GenPrivKey().PubKey()
delPk2 = ed25519.GenPrivKey().PubKey()
delPk3 = ed25519.GenPrivKey().PubKey()
delAddr1 = sdk.AccAddress(delPk1.Address())
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())
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())
// TODO move to common testing package for all modules
// test addresses
TestAddrs = []sdk.AccAddress{
delAddr1, delAddr2, delAddr3,
valAccAddr1, valAccAddr2, valAccAddr3,
}
emptyDelAddr sdk.AccAddress
emptyValAddr sdk.ValAddress
emptyPubkey crypto.PubKey
stakeDenom = "stake"
feeDenom = "fee"
)
func MakeTestCodec() *codec.Codec {
var cdc = codec.New()
auth.RegisterCodec(cdc)
vesting.RegisterCodec(cdc)
types.RegisterCodec(cdc)
supply.RegisterCodec(cdc)
staking.RegisterCodec(cdc)
sdk.RegisterCodec(cdc)
codec.RegisterCrypto(cdc)
return cdc
}
// test common should produce a staking keeper, a supply keeper, a bank keeper, an auth keeper, a validatorvesting keeper, a context,
func CreateTestInput(t *testing.T, isCheckTx bool, initPower int64) (sdk.Context, auth.AccountKeeper, bank.Keeper, staking.Keeper, supply.Keeper, Keeper) {
initTokens := sdk.TokensFromConsensusPower(initPower)
keyAcc := sdk.NewKVStoreKey(auth.StoreKey)
keyStaking := sdk.NewKVStoreKey(staking.StoreKey)
keySupply := sdk.NewKVStoreKey(supply.StoreKey)
keyParams := sdk.NewKVStoreKey(params.StoreKey)
tkeyParams := sdk.NewTransientStoreKey(params.TStoreKey)
keyValidatorVesting := sdk.NewKVStoreKey(types.StoreKey)
db := dbm.NewMemDB()
ms := store.NewCommitMultiStore(db)
ms.MountStoreWithDB(keyAcc, sdk.StoreTypeIAVL, db)
ms.MountStoreWithDB(keySupply, sdk.StoreTypeIAVL, db)
ms.MountStoreWithDB(keyValidatorVesting, sdk.StoreTypeIAVL, db)
ms.MountStoreWithDB(keyStaking, sdk.StoreTypeIAVL, db)
ms.MountStoreWithDB(keyParams, sdk.StoreTypeIAVL, db)
ms.MountStoreWithDB(tkeyParams, sdk.StoreTypeTransient, db)
require.Nil(t, ms.LoadLatestVersion())
ctx := sdk.NewContext(ms, abci.Header{ChainID: "foo-chain"}, isCheckTx, log.NewNopLogger())
feeCollectorAcc := supply.NewEmptyModuleAccount(auth.FeeCollectorName)
notBondedPool := supply.NewEmptyModuleAccount(staking.NotBondedPoolName, supply.Burner, supply.Staking)
bondPool := supply.NewEmptyModuleAccount(staking.BondedPoolName, supply.Burner, supply.Staking)
validatorVestingAcc := supply.NewEmptyModuleAccount(types.ModuleName)
blacklistedAddrs := make(map[string]bool)
blacklistedAddrs[feeCollectorAcc.GetAddress().String()] = true
blacklistedAddrs[notBondedPool.GetAddress().String()] = true
blacklistedAddrs[bondPool.GetAddress().String()] = true
blacklistedAddrs[validatorVestingAcc.GetAddress().String()] = true
cdc := MakeTestCodec()
pk := params.NewKeeper(cdc, keyParams, tkeyParams, params.DefaultCodespace)
accountKeeper := auth.NewAccountKeeper(cdc, keyAcc, pk.Subspace(auth.DefaultParamspace), auth.ProtoBaseAccount)
bankKeeper := bank.NewBaseKeeper(accountKeeper, pk.Subspace(bank.DefaultParamspace), bank.DefaultCodespace, blacklistedAddrs)
maccPerms := map[string][]string{
auth.FeeCollectorName: nil,
staking.NotBondedPoolName: {supply.Burner, supply.Staking},
staking.BondedPoolName: {supply.Burner, supply.Staking},
types.ModuleName: {supply.Burner},
}
supplyKeeper := supply.NewKeeper(cdc, keySupply, accountKeeper, bankKeeper, maccPerms)
stakingKeeper := staking.NewKeeper(cdc, keyStaking, supplyKeeper, pk.Subspace(staking.DefaultParamspace), staking.DefaultCodespace)
stakingKeeper.SetParams(ctx, staking.DefaultParams())
keeper := NewKeeper(cdc, keyValidatorVesting, accountKeeper, bankKeeper, supplyKeeper, stakingKeeper)
initCoins := sdk.NewCoins(sdk.NewCoin(stakingKeeper.BondDenom(ctx), initTokens))
totalSupply := sdk.NewCoins(sdk.NewCoin(stakingKeeper.BondDenom(ctx), initTokens.MulRaw(int64(len(TestAddrs)))))
supplyKeeper.SetSupply(ctx, supply.NewSupply(totalSupply))
// fill all the addresses with some coins, set the loose pool tokens simultaneously
for _, addr := range TestAddrs {
_, err := bankKeeper.AddCoins(ctx, addr, initCoins)
require.Nil(t, err)
}
// set module accounts
keeper.supplyKeeper.SetModuleAccount(ctx, feeCollectorAcc)
keeper.supplyKeeper.SetModuleAccount(ctx, notBondedPool)
keeper.supplyKeeper.SetModuleAccount(ctx, bondPool)
return ctx, accountKeeper, bankKeeper, stakingKeeper, supplyKeeper, keeper
}
func ValidatorVestingTestAccount() *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(1)[0]
testPk := types.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 := types.NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90)
err := vva.Validate()
if err != nil {
panic(err)
}
return vva
}
func ValidatorVestingDelegatorTestAccount(startTime time.Time) *types.ValidatorVestingAccount {
periods := vesting.VestingPeriods{
vesting.VestingPeriod{PeriodLength: int64(12 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(stakeDenom, 30000000)}},
vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(stakeDenom, 15000000)}},
vesting.VestingPeriod{PeriodLength: int64(6 * 60 * 60), VestingAmount: sdk.Coins{sdk.NewInt64Coin(stakeDenom, 15000000)}},
}
testAddr := types.CreateTestAddrs(1)[0]
testPk := types.CreateTestPubKeys(1)[0]
testConsAddr := sdk.ConsAddress(testPk.Address())
origCoins := sdk.Coins{sdk.NewInt64Coin(stakeDenom, 60000000)}
bacc := auth.NewBaseAccountWithAddress(testAddr)
bacc.SetCoins(origCoins)
vva := types.NewValidatorVestingAccount(&bacc, startTime.Unix(), periods, testConsAddr, nil, 90)
err := vva.Validate()
if err != nil {
panic(err)
}
return vva
}
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{})
sk.SetValidator(ctx, val1)
sk.SetValidator(ctx, val2)
sk.SetValidator(ctx, val3)
sk.SetValidatorByConsAddr(ctx, val1)
sk.SetValidatorByConsAddr(ctx, val2)
sk.SetValidatorByConsAddr(ctx, val3)
sk.SetNewValidatorByPowerIndex(ctx, val1)
sk.SetNewValidatorByPowerIndex(ctx, val2)
sk.SetNewValidatorByPowerIndex(ctx, val3)
_, _ = sk.Delegate(ctx, valAccAddr1, sdk.TokensFromConsensusPower(powers[0]), sdk.Unbonded, val1, true)
_, _ = sk.Delegate(ctx, valAccAddr2, sdk.TokensFromConsensusPower(powers[1]), sdk.Unbonded, val2, true)
_, _ = sk.Delegate(ctx, valAccAddr3, sdk.TokensFromConsensusPower(powers[2]), sdk.Unbonded, val3, true)
_ = staking.EndBlocker(ctx, sk)
}

View File

@ -0,0 +1,19 @@
package types
import (
"github.com/cosmos/cosmos-sdk/codec"
)
// RegisterCodec registers concrete types on the codec
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(&ValidatorVestingAccount{}, "cosmos-sdk/ValidatorVestingAccount", nil)
}
// ModuleCdc module wide codec
var ModuleCdc *codec.Codec
func init() {
ModuleCdc = codec.New()
RegisterCodec(ModuleCdc)
ModuleCdc.Seal()
}

View File

@ -0,0 +1,38 @@
package types
import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth/exported"
stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
)
// AccountKeeper defines the expected account keeper (noalias)
type AccountKeeper interface {
GetAccount(sdk.Context, sdk.AccAddress) exported.Account
SetAccount(sdk.Context, exported.Account)
}
// BankKeeper defines the expected bank keeper (noalias)
type BankKeeper interface {
SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) sdk.Error
}
// StakingKeeper defines the expected staking keeper (noalias)
type StakingKeeper interface {
IterateDelegations(ctx sdk.Context, delegator sdk.AccAddress,
fn func(index int64, delegation stakingexported.DelegationI) (stop bool))
Undelegate(
ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, sharesAmount sdk.Dec,
) (time.Time, sdk.Error)
}
// SupplyKeeper defines the expected supply keeper for module accounts (noalias)
type SupplyKeeper interface {
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) sdk.Error
BurnCoins(ctx sdk.Context, name string, amt sdk.Coins) sdk.Error
SetModuleAccount(sdk.Context, supplyexported.ModuleAccountI)
}

View File

@ -0,0 +1,22 @@
package types
import (
"github.com/cosmos/cosmos-sdk/x/auth/exported"
)
// GenesisState - all auth state that must be provided at genesis
type GenesisState struct {
Accounts exported.GenesisAccounts `json:"accounts" yaml:"accounts"`
}
// NewGenesisState - Create a new genesis state
func NewGenesisState(accounts exported.GenesisAccounts) GenesisState {
return GenesisState{
Accounts: accounts,
}
}
// DefaultGenesisState - Return a default genesis state
func DefaultGenesisState() GenesisState {
return NewGenesisState(exported.GenesisAccounts{})
}

View File

@ -0,0 +1,23 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
// ModuleName name used throughout module
ModuleName = "validatorvesting"
// StoreKey to be used when creating the KVStore
StoreKey = ModuleName
)
var (
// ValidatorVestingAccountPrefix store prefix for validator vesting accounts
ValidatorVestingAccountPrefix = []byte{0x01}
)
// ValidatorVestingAccountKey returns the account address bytes prefixed by ValidatorVestingAccountPrefix
func ValidatorVestingAccountKey(addr sdk.AccAddress) []byte {
return append(ValidatorVestingAccountPrefix, addr.Bytes()...)
}

View File

@ -0,0 +1,82 @@
package types // noalias
import (
"bytes"
"encoding/hex"
"strconv"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/ed25519"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// nolint: unparam
func CreateTestAddrs(numAddrs int) []sdk.AccAddress {
var addresses []sdk.AccAddress
var buffer bytes.Buffer
// start at 100 so we can make up to 999 test addresses with valid test addresses
for i := 100; i < (numAddrs + 100); i++ {
numString := strconv.Itoa(i)
buffer.WriteString("A58856F0FD53BF058B4909A21AEC019107BA6") //base address string
buffer.WriteString(numString) //adding on final two digits to make addresses unique
res, _ := sdk.AccAddressFromHex(buffer.String())
bech := res.String()
addresses = append(addresses, TestAddr(buffer.String(), bech))
buffer.Reset()
}
return addresses
}
// TestAddr for incode address generation
func TestAddr(addr string, bech string) sdk.AccAddress {
res, err := sdk.AccAddressFromHex(addr)
if err != nil {
panic(err)
}
bechexpected := res.String()
if bech != bechexpected {
panic("Bech encoding doesn't match reference")
}
bechres, err := sdk.AccAddressFromBech32(bech)
if err != nil {
panic(err)
}
if !bytes.Equal(bechres, res) {
panic("Bech decode and hex decode don't match")
}
return res
}
// nolint: unparam
func CreateTestPubKeys(numPubKeys int) []crypto.PubKey {
var publicKeys []crypto.PubKey
var buffer bytes.Buffer
//start at 10 to avoid changing 1 to 01, 2 to 02, etc
for i := 100; i < (numPubKeys + 100); i++ {
numString := strconv.Itoa(i)
buffer.WriteString("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AF") //base pubkey string
buffer.WriteString(numString) //adding on final two digits to make pubkeys unique
publicKeys = append(publicKeys, NewPubKey(buffer.String()))
buffer.Reset()
}
return publicKeys
}
// NewPubKey for incode pubkey generation
func NewPubKey(pk string) (res crypto.PubKey) {
pkBytes, err := hex.DecodeString(pk)
if err != nil {
panic(err)
}
//res, err = crypto.PubKeyFromBytes(pkBytes)
var pkEd ed25519.PubKeyEd25519
copy(pkEd[:], pkBytes[:])
return pkEd
}

View File

@ -0,0 +1,231 @@
package types
import (
"errors"
"time"
"gopkg.in/yaml.v2"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
vestexported "github.com/cosmos/cosmos-sdk/x/auth/vesting/exported"
)
// Assert ValidatorVestingAccount implements the vestexported.VestingAccount interface
// Assert
var _ vestexported.VestingAccount = (*ValidatorVestingAccount)(nil)
var _ authexported.GenesisAccount = (*ValidatorVestingAccount)(nil)
// Register the ValidatorVestingAccount type on the auth module codec
func init() {
auth.RegisterAccountTypeCodec(&ValidatorVestingAccount{}, "cosmos-sdk/ValidatorVestingAccount")
}
// ValidatorVestingAccount implements the VestingAccount interface. It
// conditionally vests by unlocking coins during each specified period, provided
// that the validator address has validated at least **threshold** blocks during
// the previous vesting period. If the validator has not vested at least the threshold,
// the coins are returned to the return address, or burned if the return address is null.
type ValidatorVestingAccount struct {
*vesting.PeriodicVestingAccount
ValidatorAddress sdk.ConsAddress `json:"validator_address" yaml:"validator_address"`
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"`
DebtAfterFailedVesting sdk.Coins
}
// NewValidatorVestingAccountRaw creates a new ValidatorVestingAccount object from BaseVestingAccount
func NewValidatorVestingAccountRaw(bva *vesting.BaseVestingAccount,
startTime int64, periods vesting.VestingPeriods, validatorAddress sdk.ConsAddress, returnAddress sdk.AccAddress, signingThreshold int64) *ValidatorVestingAccount {
cva := &vesting.ContinuousVestingAccount{
StartTime: startTime,
BaseVestingAccount: bva,
}
pva := &vesting.PeriodicVestingAccount{
ContinuousVestingAccount: cva,
VestingPeriods: periods,
}
var vestingPeriodProgress = make([]int, len(periods))
return &ValidatorVestingAccount{
PeriodicVestingAccount: pva,
ValidatorAddress: validatorAddress,
ReturnAddress: returnAddress,
SigningThreshold: signingThreshold,
MissingSignCount: []int64{0, 0},
VestingPeriodProgress: vestingPeriodProgress,
DebtAfterFailedVesting: sdk.NewCoins(),
}
}
// NewValidatorVestingAccount creates a ValidatorVestingAccount object from a BaseAccount
func NewValidatorVestingAccount(baseAcc *auth.BaseAccount, startTime int64, periods vesting.VestingPeriods, validatorAddress sdk.ConsAddress, returnAddress sdk.AccAddress, signingThreshold int64) *ValidatorVestingAccount {
endTime := startTime
for _, p := range periods {
endTime += p.PeriodLength
}
baseVestingAcc := &vesting.BaseVestingAccount{
BaseAccount: baseAcc,
OriginalVesting: baseAcc.Coins,
EndTime: endTime,
}
cva := &vesting.ContinuousVestingAccount{
StartTime: startTime,
BaseVestingAccount: baseVestingAcc,
}
pva := &vesting.PeriodicVestingAccount{
ContinuousVestingAccount: cva,
VestingPeriods: periods,
}
var vestingPeriodProgress = make([]int, len(periods))
debt := sdk.NewCoins()
return &ValidatorVestingAccount{
PeriodicVestingAccount: pva,
ValidatorAddress: validatorAddress,
ReturnAddress: returnAddress,
SigningThreshold: signingThreshold,
MissingSignCount: []int64{0, 0},
VestingPeriodProgress: vestingPeriodProgress,
DebtAfterFailedVesting: debt,
}
}
// GetVestedCoins returns the total number of vested coins.
func (vva ValidatorVestingAccount) GetVestedCoins(blockTime time.Time) sdk.Coins {
var vestedCoins sdk.Coins
if blockTime.Unix() <= vva.StartTime {
return vestedCoins
}
currentPeriodStartTime := vva.StartTime
numberPeriods := len(vva.VestingPeriods)
for i := 0; i < numberPeriods; i++ {
x := blockTime.Unix() - currentPeriodStartTime
if x >= vva.VestingPeriods[i].PeriodLength {
vestedSuccess := vva.VestingPeriodProgress[i] > 0
if vestedSuccess {
vestedCoins = vestedCoins.Add(vva.VestingPeriods[i].VestingAmount)
}
currentPeriodStartTime += vva.VestingPeriods[i].PeriodLength
} else {
break
}
}
return vestedCoins
}
// 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 {
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 vestedFailure {
failedVestedCoins = failedVestedCoins.Add(vva.VestingPeriods[i].VestingAmount)
}
currentPeriodStartTime += vva.VestingPeriods[i].PeriodLength
} else {
break
}
}
return failedVestedCoins
}
// 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))
}
// 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)
}
// TrackDelegation tracks a desired delegation amount by setting the appropriate
// values for the amount of delegated vesting, delegated free, and reducing the
// overall amount of base coins.
func (vva *ValidatorVestingAccount) TrackDelegation(blockTime time.Time, amount sdk.Coins) {
vva.BaseVestingAccount.TrackDelegation(vva.GetVestingCoins(blockTime), amount)
}
// Validate checks for errors on the account fields
func (vva ValidatorVestingAccount) Validate() error {
if vva.SigningThreshold > 100 || vva.SigningThreshold < 0 {
return errors.New("signing threshold must be between 0 and 100")
}
if vva.ReturnAddress.Equals(vva.Address) {
return errors.New("return address cannot be the same as the account address")
}
return vva.PeriodicVestingAccount.Validate()
}
// MarshalYAML returns the YAML representation of an account.
func (vva ValidatorVestingAccount) MarshalYAML() (interface{}, error) {
var bs []byte
var err error
var pubkey string
if vva.PubKey != nil {
pubkey, err = sdk.Bech32ifyAccPub(vva.PubKey)
if err != nil {
return nil, err
}
}
bs, err = yaml.Marshal(struct {
Address sdk.AccAddress
Coins sdk.Coins
PubKey string
AccountNumber uint64
Sequence uint64
OriginalVesting sdk.Coins
DelegatedFree sdk.Coins
DelegatedVesting sdk.Coins
EndTime int64
StartTime int64
VestingPeriods vesting.VestingPeriods
ValidatorAddress sdk.ConsAddress
ReturnAddress sdk.AccAddress
SigningThreshold int64
MissingSignCount []int64
VestingPeriodProgress []int
DebtAfterFailedVesting sdk.Coins
}{
Address: vva.Address,
Coins: vva.Coins,
PubKey: pubkey,
AccountNumber: vva.AccountNumber,
Sequence: vva.Sequence,
OriginalVesting: vva.OriginalVesting,
DelegatedFree: vva.DelegatedFree,
DelegatedVesting: vva.DelegatedVesting,
EndTime: vva.EndTime,
StartTime: vva.StartTime,
VestingPeriods: vva.VestingPeriods,
ValidatorAddress: vva.ValidatorAddress,
ReturnAddress: vva.ReturnAddress,
SigningThreshold: vva.SigningThreshold,
MissingSignCount: vva.MissingSignCount,
VestingPeriodProgress: vva.VestingPeriodProgress,
DebtAfterFailedVesting: vva.DebtAfterFailedVesting,
})
if err != nil {
return nil, err
}
return string(bs), err
}

View File

@ -0,0 +1,360 @@
package types
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
tmtime "github.com/tendermint/tendermint/types/time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
)
var (
stakeDenom = "stake"
feeDenom = "fee"
)
func TestGetVestedCoinsValidatorVestingAcc(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)
// require no coins vested at the beginning of the vesting schedule
vestedCoins := vva.GetVestedCoins(now)
require.Nil(t, vestedCoins)
// require no coins vested during first vesting period
vestedCoins = vva.GetVestedCoins(now.Add(6 * time.Hour))
require.Nil(t, vestedCoins)
// require 50% of coins vested after successful period 1 vesting
vva.VestingPeriodProgress[0] = 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
vestedCoins = vva.GetVestedCoins(now.Add(12 * time.Hour))
require.Nil(t, vestedCoins)
// require period 2 coins don't vest until period is over
vva.VestingPeriodProgress[0] = 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
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
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
vestedCoins = vva.GetVestedCoins(now.Add(18 * time.Hour))
require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 50)}, vestedCoins)
// require 100% of coins vested after all periods complete successfully
vva.VestingPeriodProgress[0] = 1
vva.VestingPeriodProgress[1] = 1
vva.VestingPeriodProgress[2] = 1
vestedCoins = vva.GetVestedCoins(now.Add(48 * time.Hour))
require.Equal(t, origCoins, vestedCoins)
}
func TestGetVestingCoinsValidatorVestingAcc(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)
// require all coins vesting at the beginning of the vesting schedule
vestingCoins := vva.GetVestingCoins(now)
require.Equal(t, origCoins, vestingCoins)
// require all coins vesting during first vesting period
vestingCoins = vva.GetVestingCoins(now.Add(6 * time.Hour))
require.Equal(t, origCoins, vestingCoins)
// require 50% of coins vesting after successful period 1 vesting
vva.VestingPeriodProgress[0] = 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
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
// should never happen, but still won't affect vesting balance
vva.VestingPeriodProgress[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
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
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
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
vestingCoins = vva.GetVestingCoins(now.Add(48 * time.Hour))
require.Nil(t, vestingCoins)
}
func TestSpendableCoinsValidatorVestingAccount(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)
// require that there exist no spendable coins at the beginning of the vesting schedule
spendableCoins := vva.SpendableCoins(now)
require.Nil(t, spendableCoins)
// require that all vested coins (50%) are spendable when period 1 completes successfully
vva.VestingPeriodProgress[0] = 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)
// 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
spendableCoins = vva.SpendableCoins(now.Add(12 * time.Hour))
require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 500), sdk.NewInt64Coin(stakeDenom, 100)}, spendableCoins)
// spend all spendable coins
vva.SetCoins(vva.GetCoins().Sub(spendableCoins))
// require that no more coins are spendable
spendableCoins = vva.SpendableCoins(now.Add(12 * time.Hour))
require.Nil(t, spendableCoins)
}
func TestTrackDelegationValidatorVestingAcc(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.TrackDelegation(now, origCoins)
require.Equal(t, origCoins, vva.DelegatedVesting)
require.Nil(t, vva.DelegatedFree)
require.Nil(t, vva.GetCoins())
// require the ability to delegate all vesting coins (50%) and all vested coins (50%)
bacc.SetCoins(origCoins)
vva = NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90)
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.Nil(t, vva.DelegatedFree)
vva.VestingPeriodProgress[0] = 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)
require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000)}, vva.GetCoins())
// require no modifications when delegation amount is zero or not enough funds
bacc.SetCoins(origCoins)
vva = NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90)
require.Panics(t, func() {
vva.TrackDelegation(now.Add(24*time.Hour), sdk.Coins{sdk.NewInt64Coin(stakeDenom, 1000000)})
})
require.Nil(t, vva.DelegatedVesting)
require.Nil(t, vva.DelegatedFree)
require.Equal(t, origCoins, vva.GetCoins())
}
func TestTrackUndelegationPeriodicVestingAcc(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)
// require ability to delegate then undelegate all coins.
vva.TrackDelegation(now, origCoins)
vva.TrackUndelegation(origCoins)
require.Nil(t, vva.DelegatedFree)
require.Nil(t, vva.DelegatedVesting)
require.Equal(t, origCoins, vva.GetCoins())
// 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.TrackDelegation(now.Add(24*time.Hour), origCoins)
vva.TrackUndelegation(origCoins)
require.Nil(t, vva.DelegatedFree)
require.Nil(t, vva.DelegatedVesting)
require.Equal(t, origCoins, vva.GetCoins())
// require panic and no modifications when attempting to undelegate zero coins
bacc.SetCoins(origCoins)
vva = NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90)
require.Panics(t, func() {
vva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 0)})
})
require.Nil(t, vva.DelegatedFree)
require.Nil(t, vva.DelegatedVesting)
require.Equal(t, origCoins, vva.GetCoins())
// successfuly vest period 1 and delegate to two validators
vva = NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 90)
vva.VestingPeriodProgress[0] = 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)})
// undelegate from one validator that got slashed 50%
vva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)})
require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}, vva.DelegatedFree)
require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)}, vva.DelegatedVesting)
require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 25)}, vva.GetCoins())
// undelegate from the other validator that did not get slashed
vva.TrackUndelegation(sdk.Coins{sdk.NewInt64Coin(stakeDenom, 50)})
require.Nil(t, vva.DelegatedFree)
require.Equal(t, sdk.Coins{sdk.NewInt64Coin(stakeDenom, 25)}, vva.DelegatedVesting)
require.Equal(t, sdk.Coins{sdk.NewInt64Coin(feeDenom, 1000), sdk.NewInt64Coin(stakeDenom, 75)}, vva.GetCoins())
}
func TestGenesisAccountValidate(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)
tests := []struct {
name string
acc authexported.GenesisAccount
expErr error
}{
{
"valid validator vesting account",
NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 100),
nil,
},
{
"invalid signing threshold",
NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, -1),
errors.New("signing threshold must be between 0 and 100"),
},
{
"invalid signing threshold",
NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, nil, 120),
errors.New("signing threshold must be between 0 and 100"),
},
{
"invalid return address",
NewValidatorVestingAccount(&bacc, now.Unix(), periods, testConsAddr, testAddr, 90),
errors.New("return address cannot be the same as the account address"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.acc.Validate()
require.Equal(t, tt.expErr, err)
})
}
}