Merge PR #1168: Governance MVP

This commit is contained in:
Sunny Aggarwal 2018-06-21 17:19:14 -07:00 committed by Christopher Goes
parent d08b916f01
commit dc2c8f900b
32 changed files with 3396 additions and 25 deletions

4
.gitignore vendored
View File

@ -28,5 +28,9 @@ profile.out
*.log
vagrant
# IDE
.idea/
*.iml
# Graphviz
dependency-graph.png

View File

@ -14,6 +14,10 @@ FEATURES
* [gaiacli] You can now attach a simple text-only memo to any transaction, with the `--memo` flag
* [lcd] Queried TXs now include the tx hash to identify each tx
* [mockapp] CompleteSetup() no longer takes a testing parameter
* [governance] Implemented MVP
* Supported proposal types: just binary (pass/fail) TextProposals for now
* Proposals need deposits to be votable; deposits are burned if proposal fails
* Delegators delegate votes to validator by default but can override (for their stake)
FIXES
* \#1259 - fix bug where certain tests that could have a nil pointer in defer

View File

@ -24,6 +24,7 @@ import (
tests "github.com/cosmos/cosmos-sdk/tests"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/gov"
"github.com/cosmos/cosmos-sdk/x/stake"
stakerest "github.com/cosmos/cosmos-sdk/x/stake/client/rest"
)
@ -423,6 +424,96 @@ func TestBonding(t *testing.T) {
}
func TestSubmitProposal(t *testing.T) {
name, password := "test", "1234567890"
addr, seed := CreateAddr(t, "test", password, GetKB(t))
cleanup, _, port := InitializeTestLCD(t, 1, []sdk.Address{addr})
defer cleanup()
// create SubmitProposal TX
resultTx := doSubmitProposal(t, port, seed, name, password, addr)
tests.WaitForHeight(resultTx.Height+1, port)
// check if tx was commited
assert.Equal(t, uint32(0), resultTx.CheckTx.Code)
assert.Equal(t, uint32(0), resultTx.DeliverTx.Code)
var proposalID int64
cdc.UnmarshalBinaryBare(resultTx.DeliverTx.GetData(), &proposalID)
// query proposal
proposal := getProposal(t, port, proposalID)
assert.Equal(t, "Test", proposal.Title)
}
func TestDeposit(t *testing.T) {
name, password := "test", "1234567890"
addr, seed := CreateAddr(t, "test", password, GetKB(t))
cleanup, _, port := InitializeTestLCD(t, 1, []sdk.Address{addr})
defer cleanup()
// create SubmitProposal TX
resultTx := doSubmitProposal(t, port, seed, name, password, addr)
tests.WaitForHeight(resultTx.Height+1, port)
// check if tx was commited
assert.Equal(t, uint32(0), resultTx.CheckTx.Code)
assert.Equal(t, uint32(0), resultTx.DeliverTx.Code)
var proposalID int64
cdc.UnmarshalBinaryBare(resultTx.DeliverTx.GetData(), &proposalID)
// query proposal
proposal := getProposal(t, port, proposalID)
assert.Equal(t, "Test", proposal.Title)
// create SubmitProposal TX
resultTx = doDeposit(t, port, seed, name, password, addr, proposalID)
tests.WaitForHeight(resultTx.Height+1, port)
// query proposal
proposal = getProposal(t, port, proposalID)
assert.True(t, proposal.TotalDeposit.IsEqual(sdk.Coins{sdk.NewCoin("steak", 10)}))
}
func TestVote(t *testing.T) {
name, password := "test", "1234567890"
addr, seed := CreateAddr(t, "test", password, GetKB(t))
cleanup, _, port := InitializeTestLCD(t, 1, []sdk.Address{addr})
defer cleanup()
// create SubmitProposal TX
resultTx := doSubmitProposal(t, port, seed, name, password, addr)
tests.WaitForHeight(resultTx.Height+1, port)
// check if tx was commited
assert.Equal(t, uint32(0), resultTx.CheckTx.Code)
assert.Equal(t, uint32(0), resultTx.DeliverTx.Code)
var proposalID int64
cdc.UnmarshalBinaryBare(resultTx.DeliverTx.GetData(), &proposalID)
// query proposal
proposal := getProposal(t, port, proposalID)
assert.Equal(t, "Test", proposal.Title)
// create SubmitProposal TX
resultTx = doDeposit(t, port, seed, name, password, addr, proposalID)
tests.WaitForHeight(resultTx.Height+1, port)
// query proposal
proposal = getProposal(t, port, proposalID)
assert.Equal(t, gov.StatusToString(gov.StatusVotingPeriod), proposal.Status)
// create SubmitProposal TX
resultTx = doVote(t, port, seed, name, password, addr, proposalID)
tests.WaitForHeight(resultTx.Height+1, port)
vote := getVote(t, port, proposalID, addr)
assert.Equal(t, proposalID, vote.ProposalID)
assert.Equal(t, gov.VoteOptionToString(gov.OptionYes), vote.Option)
}
//_____________________________________________________________________________
// get the account to get the sequence
func getAccount(t *testing.T, port string, addr sdk.Address) auth.Account {
@ -602,3 +693,129 @@ func getValidators(t *testing.T, port string) []stakerest.StakeValidatorOutput {
require.Nil(t, err)
return validators
}
func getProposal(t *testing.T, port string, proposalID int64) gov.ProposalRest {
res, body := Request(t, port, "GET", fmt.Sprintf("/gov/proposals/%d", proposalID), nil)
require.Equal(t, http.StatusOK, res.StatusCode, body)
var proposal gov.ProposalRest
err := cdc.UnmarshalJSON([]byte(body), &proposal)
require.Nil(t, err)
return proposal
}
func getVote(t *testing.T, port string, proposalID int64, voterAddr sdk.Address) gov.VoteRest {
bechVoterAddr := sdk.MustBech32ifyAcc(voterAddr)
res, body := Request(t, port, "GET", fmt.Sprintf("/gov/votes/%d/%s", proposalID, bechVoterAddr), nil)
require.Equal(t, http.StatusOK, res.StatusCode, body)
var vote gov.VoteRest
err := cdc.UnmarshalJSON([]byte(body), &vote)
require.Nil(t, err)
return vote
}
func doSubmitProposal(t *testing.T, port, seed, name, password string, proposerAddr sdk.Address) (resultTx ctypes.ResultBroadcastTxCommit) {
// get the account to get the sequence
acc := getAccount(t, port, proposerAddr)
accnum := acc.GetAccountNumber()
sequence := acc.GetSequence()
chainID := viper.GetString(client.FlagChainID)
bechProposerAddr := sdk.MustBech32ifyAcc(proposerAddr)
// submitproposal
jsonStr := []byte(fmt.Sprintf(`{
"title": "Test",
"description": "test",
"proposal_type": "Text",
"proposer": "%s",
"initial_deposit": [{ "denom": "steak", "amount": 5 }],
"base_req": {
"name": "%s",
"password": "%s",
"chain_id": "%s",
"account_number": %d,
"sequence": %d,
"gas": 100000
}
}`, bechProposerAddr, name, password, chainID, accnum, sequence))
res, body := Request(t, port, "POST", "/gov/submitproposal", jsonStr)
fmt.Println(res)
require.Equal(t, http.StatusOK, res.StatusCode, body)
var results ctypes.ResultBroadcastTxCommit
err := cdc.UnmarshalJSON([]byte(body), &results)
require.Nil(t, err)
return results
}
func doDeposit(t *testing.T, port, seed, name, password string, proposerAddr sdk.Address, proposalID int64) (resultTx ctypes.ResultBroadcastTxCommit) {
// get the account to get the sequence
acc := getAccount(t, port, proposerAddr)
accnum := acc.GetAccountNumber()
sequence := acc.GetSequence()
chainID := viper.GetString(client.FlagChainID)
bechProposerAddr := sdk.MustBech32ifyAcc(proposerAddr)
// deposit on proposal
jsonStr := []byte(fmt.Sprintf(`{
"depositer": "%s",
"proposalID": %d,
"amount": [{ "denom": "steak", "amount": 5 }],
"base_req": {
"name": "%s",
"password": "%s",
"chain_id": "%s",
"account_number": %d,
"sequence": %d,
"gas": 100000
}
}`, bechProposerAddr, proposalID, name, password, chainID, accnum, sequence))
res, body := Request(t, port, "POST", "/gov/deposit", jsonStr)
fmt.Println(res)
require.Equal(t, http.StatusOK, res.StatusCode, body)
var results ctypes.ResultBroadcastTxCommit
err := cdc.UnmarshalJSON([]byte(body), &results)
require.Nil(t, err)
return results
}
func doVote(t *testing.T, port, seed, name, password string, proposerAddr sdk.Address, proposalID int64) (resultTx ctypes.ResultBroadcastTxCommit) {
// get the account to get the sequence
acc := getAccount(t, port, proposerAddr)
accnum := acc.GetAccountNumber()
sequence := acc.GetSequence()
chainID := viper.GetString(client.FlagChainID)
bechProposerAddr := sdk.MustBech32ifyAcc(proposerAddr)
// vote on proposal
jsonStr := []byte(fmt.Sprintf(`{
"voter": "%s",
"proposalID": %d,
"option": "Yes",
"base_req": {
"name": "%s",
"password": "%s",
"chain_id": "%s",
"account_number": %d,
"sequence": %d,
"gas": 100000
}
}`, bechProposerAddr, proposalID, name, password, chainID, accnum, sequence))
res, body := Request(t, port, "POST", "/gov/vote", jsonStr)
fmt.Println(res)
require.Equal(t, http.StatusOK, res.StatusCode, body)
var results ctypes.ResultBroadcastTxCommit
err := cdc.UnmarshalJSON([]byte(body), &results)
require.Nil(t, err)
return results
}

View File

@ -20,6 +20,7 @@ import (
"github.com/cosmos/cosmos-sdk/wire"
auth "github.com/cosmos/cosmos-sdk/x/auth/client/rest"
bank "github.com/cosmos/cosmos-sdk/x/bank/client/rest"
gov "github.com/cosmos/cosmos-sdk/x/gov/client/rest"
ibc "github.com/cosmos/cosmos-sdk/x/ibc/client/rest"
stake "github.com/cosmos/cosmos-sdk/x/stake/client/rest"
)
@ -80,5 +81,6 @@ func createHandler(cdc *wire.Codec) http.Handler {
bank.RegisterRoutes(ctx, r, cdc, kb)
ibc.RegisterRoutes(ctx, r, cdc, kb)
stake.RegisterRoutes(ctx, r, cdc, kb)
gov.RegisterRoutes(ctx, r, cdc, kb)
return r
}

View File

@ -15,6 +15,7 @@ import (
"github.com/cosmos/cosmos-sdk/wire"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/cosmos-sdk/x/gov"
"github.com/cosmos/cosmos-sdk/x/ibc"
"github.com/cosmos/cosmos-sdk/x/slashing"
"github.com/cosmos/cosmos-sdk/x/stake"
@ -41,6 +42,7 @@ type GaiaApp struct {
keyIBC *sdk.KVStoreKey
keyStake *sdk.KVStoreKey
keySlashing *sdk.KVStoreKey
keyGov *sdk.KVStoreKey
// Manage getting and setting accounts
accountMapper auth.AccountMapper
@ -49,6 +51,7 @@ type GaiaApp struct {
ibcMapper ibc.Mapper
stakeKeeper stake.Keeper
slashingKeeper slashing.Keeper
govKeeper gov.Keeper
}
func NewGaiaApp(logger log.Logger, db dbm.DB) *GaiaApp {
@ -63,6 +66,7 @@ func NewGaiaApp(logger log.Logger, db dbm.DB) *GaiaApp {
keyIBC: sdk.NewKVStoreKey("ibc"),
keyStake: sdk.NewKVStoreKey("stake"),
keySlashing: sdk.NewKVStoreKey("slashing"),
keyGov: sdk.NewKVStoreKey("gov"),
}
// define the accountMapper
@ -77,20 +81,22 @@ func NewGaiaApp(logger log.Logger, db dbm.DB) *GaiaApp {
app.ibcMapper = ibc.NewMapper(app.cdc, app.keyIBC, app.RegisterCodespace(ibc.DefaultCodespace))
app.stakeKeeper = stake.NewKeeper(app.cdc, app.keyStake, app.coinKeeper, app.RegisterCodespace(stake.DefaultCodespace))
app.slashingKeeper = slashing.NewKeeper(app.cdc, app.keySlashing, app.stakeKeeper, app.RegisterCodespace(slashing.DefaultCodespace))
app.govKeeper = gov.NewKeeper(app.cdc, app.keyGov, app.coinKeeper, app.stakeKeeper, app.RegisterCodespace(gov.DefaultCodespace))
// register message routes
app.Router().
AddRoute("bank", bank.NewHandler(app.coinKeeper)).
AddRoute("ibc", ibc.NewHandler(app.ibcMapper, app.coinKeeper)).
AddRoute("stake", stake.NewHandler(app.stakeKeeper)).
AddRoute("slashing", slashing.NewHandler(app.slashingKeeper))
AddRoute("slashing", slashing.NewHandler(app.slashingKeeper)).
AddRoute("gov", gov.NewHandler(app.govKeeper))
// initialize BaseApp
app.SetInitChainer(app.initChainer)
app.SetBeginBlocker(app.BeginBlocker)
app.SetEndBlocker(app.EndBlocker)
app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper, app.feeCollectionKeeper))
app.MountStoresIAVL(app.keyMain, app.keyAccount, app.keyIBC, app.keyStake, app.keySlashing)
app.MountStoresIAVL(app.keyMain, app.keyAccount, app.keyIBC, app.keyStake, app.keySlashing, app.keyGov)
err := app.LoadLatestVersion(app.keyMain)
if err != nil {
cmn.Exit(err.Error())
@ -106,6 +112,7 @@ func MakeCodec() *wire.Codec {
bank.RegisterWire(cdc)
stake.RegisterWire(cdc)
slashing.RegisterWire(cdc)
gov.RegisterWire(cdc)
auth.RegisterWire(cdc)
sdk.RegisterWire(cdc)
wire.RegisterCrypto(cdc)
@ -125,8 +132,11 @@ func (app *GaiaApp) BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock) ab
func (app *GaiaApp) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock {
validatorUpdates := stake.EndBlocker(ctx, app.stakeKeeper)
tags, _ := gov.EndBlocker(ctx, app.govKeeper)
return abci.ResponseEndBlock{
ValidatorUpdates: validatorUpdates,
Tags: tags,
}
}
@ -152,6 +162,8 @@ func (app *GaiaApp) initChainer(ctx sdk.Context, req abci.RequestInitChain) abci
// load the initial stake information
stake.InitGenesis(ctx, app.stakeKeeper, genesisState.StakeData)
gov.InitGenesis(ctx, app.govKeeper, gov.DefaultGenesisState())
return abci.ResponseInitChain{}
}

View File

@ -15,6 +15,7 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/wire"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/gov"
"github.com/cosmos/cosmos-sdk/x/stake"
crypto "github.com/tendermint/go-crypto"
)
@ -147,6 +148,59 @@ func TestGaiaCLICreateValidator(t *testing.T) {
assert.Equal(t, "1/1", validator.PoolShares.Amount.String())
}
func TestGaiaCLISubmitProposal(t *testing.T) {
tests.ExecuteT(t, "gaiad unsafe_reset_all")
pass := "1234567890"
executeWrite(t, "gaiacli keys delete foo", pass)
executeWrite(t, "gaiacli keys delete bar", pass)
chainID := executeInit(t, "gaiad init -o --name=foo")
executeWrite(t, "gaiacli keys add bar", pass)
// get a free port, also setup some common flags
servAddr, port, err := server.FreeTCPAddr()
require.NoError(t, err)
flags := fmt.Sprintf("--node=%v --chain-id=%v", servAddr, chainID)
// start gaiad server
proc := tests.GoExecuteT(t, fmt.Sprintf("gaiad start --rpc.laddr=%v", servAddr))
defer proc.Stop(false)
tests.WaitForStart(port)
fooAddr, _ := executeGetAddrPK(t, "gaiacli keys show foo --output=json")
fooCech, err := sdk.Bech32ifyAcc(fooAddr)
require.NoError(t, err)
fooAcc := executeGetAccount(t, fmt.Sprintf("gaiacli account %v %v", fooCech, flags))
assert.Equal(t, int64(50), fooAcc.GetCoins().AmountOf("steak").Int64())
executeWrite(t, fmt.Sprintf("gaiacli gov submitproposal %v --proposer=%v --deposit=5steak --type=Text --title=Test --description=test --name=foo", flags, fooCech), pass)
tests.WaitForNextHeightTM(port)
fooAcc = executeGetAccount(t, fmt.Sprintf("gaiacli account %v %v", fooCech, flags))
assert.Equal(t, int64(45), fooAcc.GetCoins().AmountOf("steak").Int64())
proposal1 := executeGetProposal(t, fmt.Sprintf("gaiacli gov query-proposal --proposalID=1 --output=json %v", flags))
assert.Equal(t, int64(1), proposal1.ProposalID)
assert.Equal(t, gov.StatusToString(gov.StatusDepositPeriod), proposal1.Status)
executeWrite(t, fmt.Sprintf("gaiacli gov deposit %v --depositer=%v --deposit=10steak --proposalID=1 --name=foo", flags, fooCech), pass)
tests.WaitForNextHeightTM(port)
fooAcc = executeGetAccount(t, fmt.Sprintf("gaiacli account %v %v", fooCech, flags))
assert.Equal(t, int64(35), fooAcc.GetCoins().AmountOf("steak").Int64())
proposal1 = executeGetProposal(t, fmt.Sprintf("gaiacli gov query-proposal --proposalID=1 --output=json %v", flags))
assert.Equal(t, int64(1), proposal1.ProposalID)
assert.Equal(t, gov.StatusToString(gov.StatusVotingPeriod), proposal1.Status)
executeWrite(t, fmt.Sprintf("gaiacli gov vote %v --proposalID=1 --voter=%v --option=Yes --name=foo", flags, fooCech), pass)
tests.WaitForNextHeightTM(port)
vote := executeGetVote(t, fmt.Sprintf("gaiacli gov query-vote --proposalID=1 --voter=%v --output=json %v", fooCech, flags))
assert.Equal(t, int64(1), vote.ProposalID)
assert.Equal(t, gov.VoteOptionToString(gov.OptionYes), vote.Option)
}
//___________________________________________________________________________________
// executors
@ -211,3 +265,21 @@ func executeGetValidator(t *testing.T, cmdStr string) stake.Validator {
require.NoError(t, err, "out %v\n, err %v", out, err)
return validator
}
func executeGetProposal(t *testing.T, cmdStr string) gov.ProposalRest {
out := tests.ExecuteT(t, cmdStr)
var proposal gov.ProposalRest
cdc := app.MakeCodec()
err := cdc.UnmarshalJSON([]byte(out), &proposal)
require.NoError(t, err, "out %v\n, err %v", out, err)
return proposal
}
func executeGetVote(t *testing.T, cmdStr string) gov.VoteRest {
out := tests.ExecuteT(t, cmdStr)
var vote gov.VoteRest
cdc := app.MakeCodec()
err := cdc.UnmarshalJSON([]byte(out), &vote)
require.NoError(t, err, "out %v\n, err %v", out, err)
return vote
}

View File

@ -13,6 +13,7 @@ import (
"github.com/cosmos/cosmos-sdk/version"
authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli"
bankcmd "github.com/cosmos/cosmos-sdk/x/bank/client/cli"
govcmd "github.com/cosmos/cosmos-sdk/x/gov/client/cli"
ibccmd "github.com/cosmos/cosmos-sdk/x/ibc/client/cli"
slashingcmd "github.com/cosmos/cosmos-sdk/x/slashing/client/cli"
stakecmd "github.com/cosmos/cosmos-sdk/x/stake/client/cli"
@ -101,6 +102,26 @@ func main() {
stakeCmd,
)
//Add stake commands
govCmd := &cobra.Command{
Use: "gov",
Short: "Governance and voting subcommands",
}
govCmd.AddCommand(
client.GetCommands(
govcmd.GetCmdQueryProposal("gov", cdc),
govcmd.GetCmdQueryVote("gov", cdc),
)...)
govCmd.AddCommand(
client.PostCommands(
govcmd.GetCmdSubmitProposal(cdc),
govcmd.GetCmdDeposit(cdc),
govcmd.GetCmdVote(cdc),
)...)
rootCmd.AddCommand(
govCmd,
)
//Add auth and bank commands
rootCmd.AddCommand(
client.GetCommands(

View File

@ -31,8 +31,7 @@ const (
type Procedure struct {
VotingPeriod int64 // Length of the voting period. Initial value: 2 weeks
MinDeposit int64 // Minimum deposit for a proposal to enter voting period.
VoteTypes []VoteType // Vote types available to voters.
ProposalTypes []ProposalType // Proposal types available to submitters.
ProposalTypes []string // Types available to submitters. {PlainTextProposal, SoftwareUpgradeProposal}
Threshold rational.Rational // Minimum propotion of Yes votes for proposal to pass. Initial value: 0.5
Veto rational.Rational // Minimum value of Veto votes to Total votes ratio for proposal to be vetoed. Initial value: 1/3
MaxDepositPeriod int64 // Maximum period for Atom holders to deposit on a proposal. Initial value: 2 months

View File

@ -183,8 +183,8 @@ vote on the proposal.
```go
type TxGovVote struct {
ProposalID int64 // proposalID of the proposal
Option string // option from OptionSet chosen by the voter
Address crypto.address // Address of the validator voter wants to tie its vote to
Option string // option chosen by the voter
ValidatorAddress crypto.address // Address of the validator voter wants to tie its vote to
}
```
@ -224,17 +224,15 @@ handled:
throw
validator = load(CurrentValidators, txGovVote.Address)
if !proposal.InitProcedure.OptionSet.includes(txGovVote.Option) OR
(validator == nil) then
// Throws if
// Option is not in Option Set of procedure that was active when vote opened OR if
// Address is not the address of a current validator
throw
option = load(Options, <txGovVote.ProposalID>:<sender>:<txGovVote.Address>)
if (validator == nil) then
// Throws if
// ValidatorAddress is not the address of a current validator
throw
else
option = load(Options, <txGovVote.ProposalID>:<sender>:<txGovVote.ValidatorAddress>)
if (option != nil)
// sender has already voted with the Atoms bonded to Address

View File

@ -37,6 +37,7 @@ type Validator interface {
GetOwner() Address // owner address to receive/return validators coins
GetPubKey() crypto.PubKey // validation pubkey
GetPower() Rat // validation power
GetDelegatorShares() Rat // Total out standing delegator shares
GetBondHeight() int64 // height in which the validator became active
}
@ -76,9 +77,10 @@ type Delegation interface {
// properties for the set of all delegations for a particular
type DelegationSet interface {
GetValidatorSet() ValidatorSet // validator set for which delegation set is based upon
// iterate through all delegations from one delegator by validator-address,
// execute func for each validator
IterateDelegators(Context, delegator Address,
IterateDelegations(ctx Context, delegator Address,
fn func(index int64, delegation Delegation) (stop bool))
}

View File

@ -4,6 +4,7 @@ import (
"os"
abci "github.com/tendermint/abci/types"
crypto "github.com/tendermint/go-crypto"
dbm "github.com/tendermint/tmlibs/db"
"github.com/tendermint/tmlibs/log"
@ -82,3 +83,24 @@ func (app *App) InitChainer(ctx sdk.Context, _ abci.RequestInitChain) abci.Respo
return abci.ResponseInitChain{}
}
// Generate genesis accounts loaded with coins, and returns their addresses, pubkeys, and privkeys
func CreateGenAccounts(numAccs int64, genCoins sdk.Coins) (genAccs []auth.Account, addrs []sdk.Address, pubKeys []crypto.PubKey, privKeys []crypto.PrivKey) {
for i := int64(0); i < numAccs; i++ {
privKey := crypto.GenPrivKeyEd25519()
pubKey := privKey.PubKey()
addr := pubKey.Address()
genAcc := &auth.BaseAccount{
Address: addr,
Coins: genCoins,
}
genAccs = append(genAccs, genAcc)
privKeys = append(privKeys, privKey)
pubKeys = append(pubKeys, pubKey)
addrs = append(addrs, addr)
}
return
}

251
x/gov/client/cli/tx.go Normal file
View File

@ -0,0 +1,251 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/wire"
authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli"
"github.com/cosmos/cosmos-sdk/x/gov"
"github.com/pkg/errors"
)
const (
flagProposalID = "proposalID"
flagTitle = "title"
flagDescription = "description"
flagProposalType = "type"
flagDeposit = "deposit"
flagProposer = "proposer"
flagDepositer = "depositer"
flagVoter = "voter"
flagOption = "option"
)
// submit a proposal tx
func GetCmdSubmitProposal(cdc *wire.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "submitproposal",
Short: "Submit a proposal along with an initial deposit",
RunE: func(cmd *cobra.Command, args []string) error {
title := viper.GetString(flagTitle)
description := viper.GetString(flagDescription)
strProposalType := viper.GetString(flagProposalType)
initialDeposit := viper.GetString(flagDeposit)
// get the from address from the name flag
from, err := sdk.GetAccAddressBech32(viper.GetString(flagProposer))
if err != nil {
return err
}
amount, err := sdk.ParseCoins(initialDeposit)
if err != nil {
return err
}
proposalType, err := gov.StringToProposalType(strProposalType)
if err != nil {
return err
}
// create the message
msg := gov.NewMsgSubmitProposal(title, description, proposalType, from, amount)
err = msg.ValidateBasic()
if err != nil {
return err
}
// build and sign the transaction, then broadcast to Tendermint
ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc))
res, err := ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc)
if err != nil {
return err
}
fmt.Printf("Committed at block:%d. Hash:%s.Response:%+v \n", res.Height, res.Hash.String(), res.DeliverTx)
return nil
},
}
cmd.Flags().String(flagTitle, "", "title of proposal")
cmd.Flags().String(flagDescription, "", "description of proposal")
cmd.Flags().String(flagProposalType, "", "proposalType of proposal")
cmd.Flags().String(flagDeposit, "", "deposit of proposal")
cmd.Flags().String(flagProposer, "", "proposer of proposal")
return cmd
}
// set a new Deposit transaction
func GetCmdDeposit(cdc *wire.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "deposit",
Short: "deposit tokens for activing proposal",
RunE: func(cmd *cobra.Command, args []string) error {
// get the from address from the name flag
depositer, err := sdk.GetAccAddressBech32(viper.GetString(flagDepositer))
if err != nil {
return err
}
proposalID := viper.GetInt64(flagProposalID)
amount, err := sdk.ParseCoins(viper.GetString(flagDeposit))
if err != nil {
return err
}
// create the message
msg := gov.NewMsgDeposit(depositer, proposalID, amount)
err = msg.ValidateBasic()
if err != nil {
return err
}
// build and sign the transaction, then broadcast to Tendermint
ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc))
res, err := ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc)
if err != nil {
return err
}
fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String())
return nil
},
}
cmd.Flags().String(flagProposalID, "", "proposalID of proposal depositing on")
cmd.Flags().String(flagDepositer, "", "depositer of deposit")
cmd.Flags().String(flagDeposit, "", "amount of deposit")
return cmd
}
// set a new Vote transaction
func GetCmdVote(cdc *wire.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "vote",
Short: "vote for an active proposal, options: Yes/No/NoWithVeto/Abstain",
RunE: func(cmd *cobra.Command, args []string) error {
bechVoter := viper.GetString(flagVoter)
voter, err := sdk.GetAccAddressBech32(bechVoter)
if err != nil {
return err
}
proposalID := viper.GetInt64(flagProposalID)
option := viper.GetString(flagOption)
byteVoteOption, err := gov.StringToVoteOption(option)
if err != nil {
return err
}
// create the message
msg := gov.NewMsgVote(voter, proposalID, byteVoteOption)
err = msg.ValidateBasic()
if err != nil {
return err
}
fmt.Printf("Vote[Voter:%s,ProposalID:%d,Option:%s]", bechVoter, msg.ProposalID, gov.VoteOptionToString(msg.Option))
// build and sign the transaction, then broadcast to Tendermint
ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc))
res, err := ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, []sdk.Msg{msg}, cdc)
if err != nil {
return err
}
fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String())
return nil
},
}
cmd.Flags().String(flagProposalID, "", "proposalID of proposal voting on")
cmd.Flags().String(flagVoter, "", "bech32 voter address")
cmd.Flags().String(flagOption, "", "vote option {Yes, No, NoWithVeto, Abstain}")
return cmd
}
// Command to Get a Proposal Information
func GetCmdQueryProposal(storeName string, cdc *wire.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "query-proposal",
Short: "query proposal details",
RunE: func(cmd *cobra.Command, args []string) error {
proposalID := viper.GetInt64(flagProposalID)
ctx := context.NewCoreContextFromViper()
res, err := ctx.QueryStore(gov.KeyProposal(proposalID), storeName)
if len(res) == 0 || err != nil {
return errors.Errorf("proposalID [%d] is not existed", proposalID)
}
var proposal gov.Proposal
cdc.MustUnmarshalBinary(res, &proposal)
proposalRest := gov.ProposalToRest(proposal)
output, err := wire.MarshalJSONIndent(cdc, proposalRest)
if err != nil {
return err
}
fmt.Println(string(output))
return nil
},
}
cmd.Flags().String(flagProposalID, "", "proposalID of proposal being queried")
return cmd
}
// Command to Get a Proposal Information
func GetCmdQueryVote(storeName string, cdc *wire.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "query-vote",
Short: "query vote",
RunE: func(cmd *cobra.Command, args []string) error {
proposalID := viper.GetInt64(flagProposalID)
voterAddr, err := sdk.GetAccAddressBech32(viper.GetString(flagVoter))
if err != nil {
return err
}
ctx := context.NewCoreContextFromViper()
res, err := ctx.QueryStore(gov.KeyVote(proposalID, voterAddr), storeName)
if len(res) == 0 || err != nil {
return errors.Errorf("proposalID [%d] does not exist", proposalID)
}
var vote gov.Vote
cdc.MustUnmarshalBinary(res, &vote)
voteRest := gov.VoteToRest(vote)
output, err := wire.MarshalJSONIndent(cdc, voteRest)
if err != nil {
return err
}
fmt.Println(string(output))
return nil
},
}
cmd.Flags().String(flagProposalID, "", "proposalID of proposal voting on")
cmd.Flags().String(flagVoter, "", "bech32 voter address")
return cmd
}

258
x/gov/client/rest/rest.go Normal file
View File

@ -0,0 +1,258 @@
package rest
import (
"fmt"
"net/http"
"strconv"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/wire"
"github.com/cosmos/cosmos-sdk/x/gov"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/tendermint/go-crypto/keys"
)
// REST Variable names
// nolint
const (
ProposalRestID = "proposalID"
RestVoter = "voterAddress"
)
// RegisterRoutes - Central function to define routes that get registered by the main application
func RegisterRoutes(ctx context.CoreContext, r *mux.Router, cdc *wire.Codec, kb keys.Keybase) {
r.HandleFunc("/gov/submitproposal", postProposalHandlerFn(cdc, kb, ctx)).Methods("POST")
r.HandleFunc("/gov/deposit", depositHandlerFn(cdc, kb, ctx)).Methods("POST")
r.HandleFunc("/gov/vote", voteHandlerFn(cdc, kb, ctx)).Methods("POST")
r.HandleFunc(fmt.Sprintf("/gov/proposals/{%s}", ProposalRestID), queryProposalHandlerFn("gov", cdc, kb, ctx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/gov/votes/{%s}/{%s}", ProposalRestID, RestVoter), queryVoteHandlerFn("gov", cdc, kb, ctx)).Methods("GET")
}
type postProposalReq struct {
BaseReq baseReq `json:"base_req"`
Title string `json:"title"` // Title of the proposal
Description string `json:"description"` // Description of the proposal
ProposalType string `json:"proposal_type"` // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal}
Proposer string `json:"proposer"` // Address of the proposer
InitialDeposit sdk.Coins `json:"initial_deposit"` // Coins to add to the proposal's deposit
}
type depositReq struct {
BaseReq baseReq `json:"base_req"`
ProposalID int64 `json:"proposalID"` // ID of the proposal
Depositer string `json:"depositer"` // Address of the depositer
Amount sdk.Coins `json:"amount"` // Coins to add to the proposal's deposit
}
type voteReq struct {
BaseReq baseReq `json:"base_req"`
Voter string `json:"voter"` // address of the voter
ProposalID int64 `json:"proposalID"` // proposalID of the proposal
Option string `json:"option"` // option from OptionSet chosen by the voter
}
func postProposalHandlerFn(cdc *wire.Codec, kb keys.Keybase, ctx context.CoreContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req postProposalReq
err := buildReq(w, r, &req)
if err != nil {
return
}
if !req.BaseReq.baseReqValidate(w) {
return
}
proposer, err := sdk.GetAccAddressBech32(req.Proposer)
if err != nil {
writeErr(&w, http.StatusBadRequest, err.Error())
return
}
proposalTypeByte, err := gov.StringToProposalType(req.ProposalType)
if err != nil {
writeErr(&w, http.StatusBadRequest, err.Error())
return
}
// create the message
msg := gov.NewMsgSubmitProposal(req.Title, req.Description, proposalTypeByte, proposer, req.InitialDeposit)
err = msg.ValidateBasic()
if err != nil {
writeErr(&w, http.StatusBadRequest, err.Error())
return
}
// sign
signAndBuild(w, ctx, req.BaseReq, msg, cdc)
}
}
func depositHandlerFn(cdc *wire.Codec, kb keys.Keybase, ctx context.CoreContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req depositReq
err := buildReq(w, r, &req)
if err != nil {
return
}
if !req.BaseReq.baseReqValidate(w) {
return
}
depositer, err := sdk.GetAccAddressBech32(req.Depositer)
if err != nil {
writeErr(&w, http.StatusBadRequest, err.Error())
return
}
// create the message
msg := gov.NewMsgDeposit(depositer, req.ProposalID, req.Amount)
err = msg.ValidateBasic()
if err != nil {
writeErr(&w, http.StatusBadRequest, err.Error())
return
}
// sign
signAndBuild(w, ctx, req.BaseReq, msg, cdc)
}
}
func voteHandlerFn(cdc *wire.Codec, kb keys.Keybase, ctx context.CoreContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req voteReq
err := buildReq(w, r, &req)
if err != nil {
return
}
if !req.BaseReq.baseReqValidate(w) {
return
}
voter, err := sdk.GetAccAddressBech32(req.Voter)
if err != nil {
writeErr(&w, http.StatusBadRequest, err.Error())
return
}
voteOptionByte, err := gov.StringToVoteOption(req.Option)
if err != nil {
writeErr(&w, http.StatusBadRequest, err.Error())
return
}
// create the message
msg := gov.NewMsgVote(voter, req.ProposalID, voteOptionByte)
err = msg.ValidateBasic()
if err != nil {
writeErr(&w, http.StatusBadRequest, err.Error())
return
}
// sign
signAndBuild(w, ctx, req.BaseReq, msg, cdc)
}
}
func queryProposalHandlerFn(storeName string, cdc *wire.Codec, kb keys.Keybase, ctx context.CoreContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
strProposalID := vars[ProposalRestID]
if len(strProposalID) == 0 {
w.WriteHeader(http.StatusBadRequest)
err := errors.New("proposalId required but not specified")
w.Write([]byte(err.Error()))
return
}
proposalID, err := strconv.ParseInt(strProposalID, 10, 64)
if err != nil {
err := errors.Errorf("proposalID [%d] is not positive", proposalID)
w.Write([]byte(err.Error()))
return
}
ctx := context.NewCoreContextFromViper()
res, err := ctx.QueryStore(gov.KeyProposal(proposalID), storeName)
if len(res) == 0 || err != nil {
err := errors.Errorf("proposalID [%d] does not exist", proposalID)
w.Write([]byte(err.Error()))
return
}
var proposal gov.Proposal
cdc.MustUnmarshalBinary(res, &proposal)
proposalRest := gov.ProposalToRest(proposal)
output, err := wire.MarshalJSONIndent(cdc, proposalRest)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
w.Write(output)
}
}
func queryVoteHandlerFn(storeName string, cdc *wire.Codec, kb keys.Keybase, ctx context.CoreContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
strProposalID := vars[ProposalRestID]
bechVoterAddr := vars[RestVoter]
if len(strProposalID) == 0 {
w.WriteHeader(http.StatusBadRequest)
err := errors.New("proposalId required but not specified")
w.Write([]byte(err.Error()))
return
}
proposalID, err := strconv.ParseInt(strProposalID, 10, 64)
if err != nil {
err := errors.Errorf("proposalID [%s] is not positive", proposalID)
w.Write([]byte(err.Error()))
return
}
voterAddr, err := sdk.GetAccAddressBech32(bechVoterAddr)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
err := errors.Errorf("'%s' needs to be bech32 encoded", RestVoter)
w.Write([]byte(err.Error()))
return
}
ctx := context.NewCoreContextFromViper()
key := []byte(gov.KeyVote(proposalID, voterAddr))
res, err := ctx.QueryStore(key, storeName)
if len(res) == 0 || err != nil {
res, err := ctx.QueryStore(gov.KeyProposal(proposalID), storeName)
if len(res) == 0 || err != nil {
err := errors.Errorf("proposalID [%d] does not exist", proposalID)
w.Write([]byte(err.Error()))
return
}
err = errors.Errorf("voter [%s] did not vote on proposalID [%d]", bechVoterAddr, proposalID)
w.Write([]byte(err.Error()))
return
}
var vote gov.Vote
cdc.MustUnmarshalBinary(res, &vote)
voteRest := gov.VoteToRest(vote)
output, err := wire.MarshalJSONIndent(cdc, voteRest)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
w.Write(output)
}
}

100
x/gov/client/rest/util.go Normal file
View File

@ -0,0 +1,100 @@
package rest
import (
"encoding/json"
"io/ioutil"
"net/http"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/wire"
"github.com/pkg/errors"
)
type baseReq struct {
Name string `json:"name"`
Password string `json:"password"`
ChainID string `json:"chain_id"`
AccountNumber int64 `json:"account_number"`
Sequence int64 `json:"sequence"`
Gas int64 `json:"gas"`
}
func buildReq(w http.ResponseWriter, r *http.Request, req interface{}) error {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
writeErr(&w, http.StatusBadRequest, err.Error())
return err
}
err = json.Unmarshal(body, &req)
if err != nil {
writeErr(&w, http.StatusBadRequest, err.Error())
return err
}
return nil
}
func (req baseReq) baseReqValidate(w http.ResponseWriter) bool {
if len(req.Name) == 0 {
writeErr(&w, http.StatusUnauthorized, "Name required but not specified")
return false
}
if len(req.Password) == 0 {
writeErr(&w, http.StatusUnauthorized, "Password required but not specified")
return false
}
if len(req.ChainID) == 0 {
writeErr(&w, http.StatusUnauthorized, "ChainID required but not specified")
return false
}
if req.AccountNumber < 0 {
writeErr(&w, http.StatusUnauthorized, "Account Number required but not specified")
return false
}
if req.Sequence < 0 {
writeErr(&w, http.StatusUnauthorized, "Sequence required but not specified")
return false
}
return true
}
func writeErr(w *http.ResponseWriter, status int, msg string) {
(*w).WriteHeader(status)
err := errors.New(msg)
(*w).Write([]byte(err.Error()))
}
// TODO: Build this function out into a more generic base-request (probably should live in client/lcd)
func signAndBuild(w http.ResponseWriter, ctx context.CoreContext, baseReq baseReq, msg sdk.Msg, cdc *wire.Codec) {
ctx = ctx.WithAccountNumber(baseReq.AccountNumber)
ctx = ctx.WithSequence(baseReq.Sequence)
ctx = ctx.WithChainID(baseReq.ChainID)
// add gas to context
ctx = ctx.WithGas(baseReq.Gas)
txBytes, err := ctx.SignAndBuild(baseReq.Name, baseReq.Password, []sdk.Msg{msg}, cdc)
if err != nil {
writeErr(&w, http.StatusUnauthorized, err.Error())
return
}
// send
res, err := ctx.BroadcastTx(txBytes)
if err != nil {
writeErr(&w, http.StatusInternalServerError, err.Error())
return
}
output, err := json.MarshalIndent(res, "", " ")
if err != nil {
writeErr(&w, http.StatusInternalServerError, err.Error())
return
}
w.Write(output)
}

90
x/gov/depositsvotes.go Normal file
View File

@ -0,0 +1,90 @@
package gov
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Type that represents VoteOption as a byte
type VoteOption = byte
//nolint
const (
OptionEmpty VoteOption = 0x00
OptionYes VoteOption = 0x01
OptionAbstain VoteOption = 0x02
OptionNo VoteOption = 0x03
OptionNoWithVeto VoteOption = 0x04
)
// Vote
type Vote struct {
Voter sdk.Address `json:"voter"` // address of the voter
ProposalID int64 `json:"proposal_id"` // proposalID of the proposal
Option VoteOption `json:"option"` // option from OptionSet chosen by the voter
}
// Deposit
type Deposit struct {
Depositer sdk.Address `json:"depositer"` // Address of the depositer
Amount sdk.Coins `json:"amount"` // Deposit amount
}
// ProposalTypeToString for pretty prints of ProposalType
func VoteOptionToString(option VoteOption) string {
switch option {
case OptionYes:
return "Yes"
case OptionAbstain:
return "Abstain"
case OptionNo:
return "No"
case OptionNoWithVeto:
return "NoWithVeto"
default:
return ""
}
}
func validVoteOption(option VoteOption) bool {
if option == OptionYes ||
option == OptionAbstain ||
option == OptionNo ||
option == OptionNoWithVeto {
return true
}
return false
}
// String to proposalType byte. Returns ff if invalid.
func StringToVoteOption(str string) (VoteOption, sdk.Error) {
switch str {
case "Yes":
return OptionYes, nil
case "Abstain":
return OptionAbstain, nil
case "No":
return OptionNo, nil
case "NoWithVeto":
return OptionNoWithVeto, nil
default:
return VoteOption(0xff), ErrInvalidVote(DefaultCodespace, str)
}
}
//-----------------------------------------------------------
// Rest Votes
type VoteRest struct {
Voter string `json:"voter"` // address of the voter
ProposalID int64 `json:"proposal_id"` // proposalID of the proposal
Option string `json:"option"`
}
// Turn any Vote to a ProposalRest
func VoteToRest(vote Vote) VoteRest {
bechAddr, _ := sdk.Bech32ifyAcc(vote.Voter)
return VoteRest{
Voter: bechAddr,
ProposalID: vote.ProposalID,
Option: VoteOptionToString(vote.Option),
}
}

168
x/gov/endblocker_test.go Normal file
View File

@ -0,0 +1,168 @@
package gov
import (
"testing"
"github.com/stretchr/testify/assert"
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/abci/types"
)
func TestTickExpiredDepositPeriod(t *testing.T) {
mapp, keeper, _, addrs, _, _ := getMockApp(t, 10)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
govHandler := NewHandler(keeper)
assert.Nil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.False(t, shouldPopInactiveProposalQueue(ctx, keeper))
newProposalMsg := NewMsgSubmitProposal("Test", "test", ProposalTypeText, addrs[0], sdk.Coins{sdk.NewCoin("steak", 5)})
res := govHandler(ctx, newProposalMsg)
assert.True(t, res.IsOK())
EndBlocker(ctx, keeper)
assert.NotNil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.False(t, shouldPopInactiveProposalQueue(ctx, keeper))
ctx = ctx.WithBlockHeight(10)
EndBlocker(ctx, keeper)
assert.NotNil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.False(t, shouldPopInactiveProposalQueue(ctx, keeper))
ctx = ctx.WithBlockHeight(250)
assert.NotNil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.True(t, shouldPopInactiveProposalQueue(ctx, keeper))
EndBlocker(ctx, keeper)
assert.Nil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.False(t, shouldPopInactiveProposalQueue(ctx, keeper))
}
func TestTickMultipleExpiredDepositPeriod(t *testing.T) {
mapp, keeper, _, addrs, _, _ := getMockApp(t, 10)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
govHandler := NewHandler(keeper)
assert.Nil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.False(t, shouldPopInactiveProposalQueue(ctx, keeper))
newProposalMsg := NewMsgSubmitProposal("Test", "test", ProposalTypeText, addrs[0], sdk.Coins{sdk.NewCoin("steak", 5)})
res := govHandler(ctx, newProposalMsg)
assert.True(t, res.IsOK())
EndBlocker(ctx, keeper)
assert.NotNil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.False(t, shouldPopInactiveProposalQueue(ctx, keeper))
ctx = ctx.WithBlockHeight(10)
EndBlocker(ctx, keeper)
assert.NotNil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.False(t, shouldPopInactiveProposalQueue(ctx, keeper))
newProposalMsg2 := NewMsgSubmitProposal("Test2", "test2", ProposalTypeText, addrs[1], sdk.Coins{sdk.NewCoin("steak", 5)})
res = govHandler(ctx, newProposalMsg2)
assert.True(t, res.IsOK())
ctx = ctx.WithBlockHeight(205)
assert.NotNil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.True(t, shouldPopInactiveProposalQueue(ctx, keeper))
EndBlocker(ctx, keeper)
assert.NotNil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.False(t, shouldPopInactiveProposalQueue(ctx, keeper))
ctx = ctx.WithBlockHeight(215)
assert.NotNil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.True(t, shouldPopInactiveProposalQueue(ctx, keeper))
EndBlocker(ctx, keeper)
assert.Nil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.False(t, shouldPopInactiveProposalQueue(ctx, keeper))
}
func TestTickPassedDepositPeriod(t *testing.T) {
mapp, keeper, _, addrs, _, _ := getMockApp(t, 10)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
govHandler := NewHandler(keeper)
assert.Nil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.False(t, shouldPopInactiveProposalQueue(ctx, keeper))
assert.Nil(t, keeper.ActiveProposalQueuePeek(ctx))
assert.False(t, shouldPopActiveProposalQueue(ctx, keeper))
newProposalMsg := NewMsgSubmitProposal("Test", "test", ProposalTypeText, addrs[0], sdk.Coins{sdk.NewCoin("steak", 5)})
res := govHandler(ctx, newProposalMsg)
assert.True(t, res.IsOK())
var proposalID int64
keeper.cdc.UnmarshalBinaryBare(res.Data, &proposalID)
EndBlocker(ctx, keeper)
assert.NotNil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.False(t, shouldPopInactiveProposalQueue(ctx, keeper))
ctx = ctx.WithBlockHeight(10)
EndBlocker(ctx, keeper)
assert.NotNil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.False(t, shouldPopInactiveProposalQueue(ctx, keeper))
newDepositMsg := NewMsgDeposit(addrs[1], proposalID, sdk.Coins{sdk.NewCoin("steak", 5)})
res = govHandler(ctx, newDepositMsg)
assert.True(t, res.IsOK())
assert.NotNil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.True(t, shouldPopInactiveProposalQueue(ctx, keeper))
assert.NotNil(t, keeper.ActiveProposalQueuePeek(ctx))
EndBlocker(ctx, keeper)
assert.Nil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.False(t, shouldPopInactiveProposalQueue(ctx, keeper))
assert.NotNil(t, keeper.ActiveProposalQueuePeek(ctx))
assert.False(t, shouldPopActiveProposalQueue(ctx, keeper))
}
func TestTickPassedVotingPeriod(t *testing.T) {
mapp, keeper, _, addrs, _, _ := getMockApp(t, 10)
SortAddresses(addrs)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
govHandler := NewHandler(keeper)
assert.Nil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.False(t, shouldPopInactiveProposalQueue(ctx, keeper))
assert.Nil(t, keeper.ActiveProposalQueuePeek(ctx))
assert.False(t, shouldPopActiveProposalQueue(ctx, keeper))
newProposalMsg := NewMsgSubmitProposal("Test", "test", ProposalTypeText, addrs[0], sdk.Coins{sdk.NewCoin("steak", 5)})
res := govHandler(ctx, newProposalMsg)
assert.True(t, res.IsOK())
var proposalID int64
keeper.cdc.UnmarshalBinaryBare(res.Data, &proposalID)
ctx = ctx.WithBlockHeight(10)
newDepositMsg := NewMsgDeposit(addrs[1], proposalID, sdk.Coins{sdk.NewCoin("steak", 5)})
res = govHandler(ctx, newDepositMsg)
assert.True(t, res.IsOK())
EndBlocker(ctx, keeper)
ctx = ctx.WithBlockHeight(215)
assert.True(t, shouldPopActiveProposalQueue(ctx, keeper))
depositsIterator := keeper.GetDeposits(ctx, proposalID)
assert.True(t, depositsIterator.Valid())
depositsIterator.Close()
assert.Equal(t, StatusVotingPeriod, keeper.GetProposal(ctx, proposalID).GetStatus())
EndBlocker(ctx, keeper)
assert.Nil(t, keeper.ActiveProposalQueuePeek(ctx))
depositsIterator = keeper.GetDeposits(ctx, proposalID)
assert.False(t, depositsIterator.Valid())
depositsIterator.Close()
assert.Equal(t, StatusRejected, keeper.GetProposal(ctx, proposalID).GetStatus())
}

67
x/gov/errors.go Normal file
View File

@ -0,0 +1,67 @@
//nolint
package gov
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
DefaultCodespace sdk.CodespaceType = 5
CodeUnknownProposal sdk.CodeType = 1
CodeInactiveProposal sdk.CodeType = 2
CodeAlreadyActiveProposal sdk.CodeType = 3
CodeAlreadyFinishedProposal sdk.CodeType = 4
CodeAddressNotStaked sdk.CodeType = 5
CodeInvalidTitle sdk.CodeType = 6
CodeInvalidDescription sdk.CodeType = 7
CodeInvalidProposalType sdk.CodeType = 8
CodeInvalidVote sdk.CodeType = 9
CodeInvalidGenesis sdk.CodeType = 10
)
//----------------------------------------
// Error constructors
func ErrUnknownProposal(codespace sdk.CodespaceType, proposalID int64) sdk.Error {
return sdk.NewError(codespace, CodeUnknownProposal, fmt.Sprintf("Unknown proposal - %d", proposalID))
}
func ErrInactiveProposal(codespace sdk.CodespaceType, proposalID int64) sdk.Error {
return sdk.NewError(codespace, CodeInactiveProposal, fmt.Sprintf("Inactive proposal - %d", proposalID))
}
func ErrAlreadyActiveProposal(codespace sdk.CodespaceType, proposalID int64) sdk.Error {
return sdk.NewError(codespace, CodeAlreadyActiveProposal, fmt.Sprintf("Proposal %d has been already active", proposalID))
}
func ErrAlreadyFinishedProposal(codespace sdk.CodespaceType, proposalID int64) sdk.Error {
return sdk.NewError(codespace, CodeAlreadyFinishedProposal, fmt.Sprintf("Proposal %d has already passed its voting period", proposalID))
}
func ErrAddressNotStaked(codespace sdk.CodespaceType, address sdk.Address) sdk.Error {
bechAddr, _ := sdk.Bech32ifyAcc(address)
return sdk.NewError(codespace, CodeAddressNotStaked, fmt.Sprintf("Address %s is not staked and is thus ineligible to vote", bechAddr))
}
func ErrInvalidTitle(codespace sdk.CodespaceType, title string) sdk.Error {
return sdk.NewError(codespace, CodeInvalidTitle, fmt.Sprintf("Proposal Title '%s' is not valid", title))
}
func ErrInvalidDescription(codespace sdk.CodespaceType, description string) sdk.Error {
return sdk.NewError(codespace, CodeInvalidDescription, fmt.Sprintf("Proposal Desciption '%s' is not valid", description))
}
func ErrInvalidProposalType(codespace sdk.CodespaceType, strProposalType string) sdk.Error {
return sdk.NewError(codespace, CodeInvalidProposalType, fmt.Sprintf("Proposal Type '%s' is not valid", strProposalType))
}
func ErrInvalidVote(codespace sdk.CodespaceType, strOption string) sdk.Error {
return sdk.NewError(codespace, CodeInvalidVote, fmt.Sprintf("'%s' is not a valid voting option", strOption))
}
func ErrInvalidGenesis(codespace sdk.CodespaceType, msg string) sdk.Error {
return sdk.NewError(codespace, CodeInvalidVote, msg)
}

37
x/gov/genesis.go Normal file
View File

@ -0,0 +1,37 @@
package gov
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// GenesisState - all staking state that must be provided at genesis
type GenesisState struct {
StartingProposalID int64 `json:"starting_proposalID"`
}
func NewGenesisState(startingProposalID int64) GenesisState {
return GenesisState{
StartingProposalID: startingProposalID,
}
}
// get raw genesis raw message for testing
func DefaultGenesisState() GenesisState {
return GenesisState{
StartingProposalID: 1,
}
}
// InitGenesis - store genesis parameters
func InitGenesis(ctx sdk.Context, k Keeper, data GenesisState) {
k.setInitialProposalID(ctx, data.StartingProposalID)
}
// WriteGenesis - output genesis parameters
func WriteGenesis(ctx sdk.Context, k Keeper) GenesisState {
initalProposalID, _ := k.getNewProposalID(ctx)
return GenesisState{
initalProposalID,
}
}

162
x/gov/handler.go Normal file
View File

@ -0,0 +1,162 @@
package gov
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Handle all "gov" type messages.
func NewHandler(keeper Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
switch msg := msg.(type) {
case MsgDeposit:
return handleMsgDeposit(ctx, keeper, msg)
case MsgSubmitProposal:
return handleMsgSubmitProposal(ctx, keeper, msg)
case MsgVote:
return handleMsgVote(ctx, keeper, msg)
default:
errMsg := "Unrecognized gov msg type"
return sdk.ErrUnknownRequest(errMsg).Result()
}
}
}
func handleMsgSubmitProposal(ctx sdk.Context, keeper Keeper, msg MsgSubmitProposal) sdk.Result {
proposal := keeper.NewTextProposal(ctx, msg.Title, msg.Description, msg.ProposalType)
err, votingStarted := keeper.AddDeposit(ctx, proposal.GetProposalID(), msg.Proposer, msg.InitialDeposit)
if err != nil {
return err.Result()
}
proposalIDBytes := keeper.cdc.MustMarshalBinaryBare(proposal.GetProposalID())
tags := sdk.NewTags(
"action", []byte("submitProposal"),
"proposer", []byte(msg.Proposer.String()),
"proposalId", proposalIDBytes,
)
if votingStarted {
tags.AppendTag("votingPeriodStart", proposalIDBytes)
}
return sdk.Result{
Data: proposalIDBytes,
Tags: tags,
}
}
func handleMsgDeposit(ctx sdk.Context, keeper Keeper, msg MsgDeposit) sdk.Result {
err, votingStarted := keeper.AddDeposit(ctx, msg.ProposalID, msg.Depositer, msg.Amount)
if err != nil {
return err.Result()
}
proposalIDBytes := keeper.cdc.MustMarshalBinaryBare(msg.ProposalID)
// TODO: Add tag for if voting period started
tags := sdk.NewTags(
"action", []byte("deposit"),
"depositer", []byte(msg.Depositer.String()),
"proposalId", proposalIDBytes,
)
if votingStarted {
tags.AppendTag("votingPeriodStart", proposalIDBytes)
}
return sdk.Result{
Tags: tags,
}
}
func handleMsgVote(ctx sdk.Context, keeper Keeper, msg MsgVote) sdk.Result {
err := keeper.AddVote(ctx, msg.ProposalID, msg.Voter, msg.Option)
if err != nil {
return err.Result()
}
proposalIDBytes := keeper.cdc.MustMarshalBinaryBare(msg.ProposalID)
tags := sdk.NewTags(
"action", []byte("vote"),
"voter", []byte(msg.Voter.String()),
"proposalId", proposalIDBytes,
)
return sdk.Result{
Tags: tags,
}
}
// Called every block, process inflation, update validator set
func EndBlocker(ctx sdk.Context, keeper Keeper) (tags sdk.Tags, nonVotingVals []sdk.Address) {
tags = sdk.NewTags()
// Delete proposals that haven't met minDeposit
for shouldPopInactiveProposalQueue(ctx, keeper) {
inactiveProposal := keeper.InactiveProposalQueuePop(ctx)
if inactiveProposal.GetStatus() == StatusDepositPeriod {
proposalIDBytes := keeper.cdc.MustMarshalBinaryBare(inactiveProposal.GetProposalID())
keeper.DeleteProposal(ctx, inactiveProposal)
tags.AppendTag("action", []byte("proposalDropped"))
tags.AppendTag("proposalId", proposalIDBytes)
}
}
var passes bool
// Check if earliest Active Proposal ended voting period yet
for shouldPopActiveProposalQueue(ctx, keeper) {
activeProposal := keeper.ActiveProposalQueuePop(ctx)
if ctx.BlockHeight() >= activeProposal.GetVotingStartBlock()+keeper.GetVotingProcedure(ctx).VotingPeriod {
passes, nonVotingVals = tally(ctx, keeper, activeProposal)
proposalIDBytes := keeper.cdc.MustMarshalBinaryBare(activeProposal.GetProposalID())
if passes {
keeper.RefundDeposits(ctx, activeProposal.GetProposalID())
activeProposal.SetStatus(StatusPassed)
tags.AppendTag("action", []byte("proposalPassed"))
tags.AppendTag("proposalId", proposalIDBytes)
} else {
keeper.DeleteDeposits(ctx, activeProposal.GetProposalID())
activeProposal.SetStatus(StatusRejected)
tags.AppendTag("action", []byte("proposalRejected"))
tags.AppendTag("proposalId", proposalIDBytes)
}
keeper.SetProposal(ctx, activeProposal)
}
}
return tags, nonVotingVals
}
func shouldPopInactiveProposalQueue(ctx sdk.Context, keeper Keeper) bool {
depositProcedure := keeper.GetDepositProcedure(ctx)
peekProposal := keeper.InactiveProposalQueuePeek(ctx)
if peekProposal == nil {
return false
} else if peekProposal.GetStatus() != StatusDepositPeriod {
return true
} else if ctx.BlockHeight() >= peekProposal.GetSubmitBlock()+depositProcedure.MaxDepositPeriod {
return true
}
return false
}
func shouldPopActiveProposalQueue(ctx sdk.Context, keeper Keeper) bool {
votingProcedure := keeper.GetVotingProcedure(ctx)
peekProposal := keeper.ActiveProposalQueuePeek(ctx)
if peekProposal == nil {
return false
} else if ctx.BlockHeight() >= peekProposal.GetVotingStartBlock()+votingProcedure.VotingPeriod {
return true
}
return false
}

407
x/gov/keeper.go Normal file
View File

@ -0,0 +1,407 @@
package gov
import (
sdk "github.com/cosmos/cosmos-sdk/types"
wire "github.com/cosmos/cosmos-sdk/wire"
"github.com/cosmos/cosmos-sdk/x/bank"
)
// Governance Keeper
type Keeper struct {
// The reference to the CoinKeeper to modify balances
ck bank.Keeper
// The ValidatorSet to get information about validators
vs sdk.ValidatorSet
// The reference to the DelegationSet to get information about delegators
ds sdk.DelegationSet
// The (unexposed) keys used to access the stores from the Context.
storeKey sdk.StoreKey
// The wire codec for binary encoding/decoding.
cdc *wire.Codec
// Reserved codespace
codespace sdk.CodespaceType
}
// NewGovernanceMapper returns a mapper that uses go-wire to (binary) encode and decode gov types.
func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, ck bank.Keeper, ds sdk.DelegationSet, codespace sdk.CodespaceType) Keeper {
return Keeper{
storeKey: key,
ck: ck,
ds: ds,
vs: ds.GetValidatorSet(),
cdc: cdc,
codespace: codespace,
}
}
// Returns the go-wire codec.
func (keeper Keeper) WireCodec() *wire.Codec {
return keeper.cdc
}
// =====================================================
// Proposals
// Creates a NewProposal
func (keeper Keeper) NewTextProposal(ctx sdk.Context, title string, description string, proposalType byte) Proposal {
proposalID, err := keeper.getNewProposalID(ctx)
if err != nil {
return nil
}
var proposal Proposal = &TextProposal{
ProposalID: proposalID,
Title: title,
Description: description,
ProposalType: proposalType,
Status: StatusDepositPeriod,
TotalDeposit: sdk.Coins{},
SubmitBlock: ctx.BlockHeight(),
VotingStartBlock: -1, // TODO: Make Time
}
keeper.SetProposal(ctx, proposal)
keeper.InactiveProposalQueuePush(ctx, proposal)
return proposal
}
// Get Proposal from store by ProposalID
func (keeper Keeper) GetProposal(ctx sdk.Context, proposalID int64) Proposal {
store := ctx.KVStore(keeper.storeKey)
bz := store.Get(KeyProposal(proposalID))
if bz == nil {
return nil
}
var proposal Proposal
keeper.cdc.MustUnmarshalBinary(bz, &proposal)
return proposal
}
// Implements sdk.AccountMapper.
func (keeper Keeper) SetProposal(ctx sdk.Context, proposal Proposal) {
store := ctx.KVStore(keeper.storeKey)
bz := keeper.cdc.MustMarshalBinary(proposal)
store.Set(KeyProposal(proposal.GetProposalID()), bz)
}
// Implements sdk.AccountMapper.
func (keeper Keeper) DeleteProposal(ctx sdk.Context, proposal Proposal) {
store := ctx.KVStore(keeper.storeKey)
store.Delete(KeyProposal(proposal.GetProposalID()))
}
func (keeper Keeper) setInitialProposalID(ctx sdk.Context, proposalID int64) sdk.Error {
store := ctx.KVStore(keeper.storeKey)
bz := store.Get(KeyNextProposalID)
if bz != nil {
return ErrInvalidGenesis(keeper.codespace, "Initial ProposalID already set")
}
bz = keeper.cdc.MustMarshalBinary(proposalID) // TODO: switch to MarshalBinaryBare when new go-amino gets added
store.Set(KeyNextProposalID, bz)
return nil
}
func (keeper Keeper) getNewProposalID(ctx sdk.Context) (proposalID int64, err sdk.Error) {
store := ctx.KVStore(keeper.storeKey)
bz := store.Get(KeyNextProposalID)
if bz == nil {
return -1, ErrInvalidGenesis(keeper.codespace, "InitialProposalID never set")
}
keeper.cdc.MustUnmarshalBinary(bz, &proposalID) // TODO: switch to UnmarshalBinaryBare when new go-amino gets added
bz = keeper.cdc.MustMarshalBinary(proposalID + 1) // TODO: switch to MarshalBinaryBare when new go-amino gets added
store.Set(KeyNextProposalID, bz)
return proposalID, nil
}
func (keeper Keeper) activateVotingPeriod(ctx sdk.Context, proposal Proposal) {
proposal.SetVotingStartBlock(ctx.BlockHeight())
proposal.SetStatus(StatusVotingPeriod)
keeper.SetProposal(ctx, proposal)
keeper.ActiveProposalQueuePush(ctx, proposal)
}
// =====================================================
// Procedures
// Gets procedure from store. TODO: move to global param store and allow for updating of this
func (keeper Keeper) GetDepositProcedure(ctx sdk.Context) DepositProcedure {
return DepositProcedure{
MinDeposit: sdk.Coins{sdk.NewCoin("steak", 10)},
MaxDepositPeriod: 200,
}
}
// Gets procedure from store. TODO: move to global param store and allow for updating of this
func (keeper Keeper) GetVotingProcedure(ctx sdk.Context) VotingProcedure {
return VotingProcedure{
VotingPeriod: 200,
}
}
// Gets procedure from store. TODO: move to global param store and allow for updating of this
func (keeper Keeper) GetTallyingProcedure(ctx sdk.Context) TallyingProcedure {
return TallyingProcedure{
Threshold: sdk.NewRat(1, 2),
Veto: sdk.NewRat(1, 3),
GovernancePenalty: sdk.NewRat(1, 100),
}
}
// =====================================================
// Votes
// Adds a vote on a specific proposal
func (keeper Keeper) AddVote(ctx sdk.Context, proposalID int64, voterAddr sdk.Address, option VoteOption) sdk.Error {
proposal := keeper.GetProposal(ctx, proposalID)
if proposal == nil {
return ErrUnknownProposal(keeper.codespace, proposalID)
}
if proposal.GetStatus() != StatusVotingPeriod {
return ErrInactiveProposal(keeper.codespace, proposalID)
}
if option != OptionYes && option != OptionAbstain && option != OptionNo && option != OptionNoWithVeto {
return ErrInvalidVote(keeper.codespace, VoteOptionToString(option))
}
vote := Vote{
ProposalID: proposalID,
Voter: voterAddr,
Option: option,
}
keeper.setVote(ctx, proposalID, voterAddr, vote)
return nil
}
// Gets the vote of a specific voter on a specific proposal
func (keeper Keeper) GetVote(ctx sdk.Context, proposalID int64, voterAddr sdk.Address) (Vote, bool) {
store := ctx.KVStore(keeper.storeKey)
bz := store.Get(KeyVote(proposalID, voterAddr))
if bz == nil {
return Vote{}, false
}
var vote Vote
keeper.cdc.MustUnmarshalBinary(bz, &vote)
return vote, true
}
func (keeper Keeper) setVote(ctx sdk.Context, proposalID int64, voterAddr sdk.Address, vote Vote) {
store := ctx.KVStore(keeper.storeKey)
bz := keeper.cdc.MustMarshalBinary(vote)
store.Set(KeyVote(proposalID, voterAddr), bz)
}
// Gets all the votes on a specific proposal
func (keeper Keeper) GetVotes(ctx sdk.Context, proposalID int64) sdk.Iterator {
store := ctx.KVStore(keeper.storeKey)
return sdk.KVStorePrefixIterator(store, KeyVotesSubspace(proposalID))
}
func (keeper Keeper) deleteVote(ctx sdk.Context, proposalID int64, voterAddr sdk.Address) {
store := ctx.KVStore(keeper.storeKey)
store.Delete(KeyVote(proposalID, voterAddr))
}
// =====================================================
// Deposits
// Gets the deposit of a specific depositer on a specific proposal
func (keeper Keeper) GetDeposit(ctx sdk.Context, proposalID int64, depositerAddr sdk.Address) (Deposit, bool) {
store := ctx.KVStore(keeper.storeKey)
bz := store.Get(KeyDeposit(proposalID, depositerAddr))
if bz == nil {
return Deposit{}, false
}
var deposit Deposit
keeper.cdc.MustUnmarshalBinary(bz, &deposit)
return deposit, true
}
func (keeper Keeper) setDeposit(ctx sdk.Context, proposalID int64, depositerAddr sdk.Address, deposit Deposit) {
store := ctx.KVStore(keeper.storeKey)
bz := keeper.cdc.MustMarshalBinary(deposit)
store.Set(KeyDeposit(proposalID, depositerAddr), bz)
}
// Adds or updates a deposit of a specific depositer on a specific proposal
// Activates voting period when appropriate
func (keeper Keeper) AddDeposit(ctx sdk.Context, proposalID int64, depositerAddr sdk.Address, depositAmount sdk.Coins) (sdk.Error, bool) {
// Checks to see if proposal exists
proposal := keeper.GetProposal(ctx, proposalID)
if proposal == nil {
return ErrUnknownProposal(keeper.codespace, proposalID), false
}
// Check if proposal is still depositable
if (proposal.GetStatus() != StatusDepositPeriod) && (proposal.GetStatus() != StatusVotingPeriod) {
return ErrAlreadyFinishedProposal(keeper.codespace, proposalID), false
}
// Subtract coins from depositers account
_, _, err := keeper.ck.SubtractCoins(ctx, depositerAddr, depositAmount)
if err != nil {
return err, false
}
// Update Proposal
proposal.SetTotalDeposit(proposal.GetTotalDeposit().Plus(depositAmount))
keeper.SetProposal(ctx, proposal)
// Check if deposit tipped proposal into voting period
// Active voting period if so
activatedVotingPeriod := false
if proposal.GetStatus() == StatusDepositPeriod && proposal.GetTotalDeposit().IsGTE(keeper.GetDepositProcedure(ctx).MinDeposit) {
keeper.activateVotingPeriod(ctx, proposal)
activatedVotingPeriod = true
}
// Add or update deposit object
currDeposit, found := keeper.GetDeposit(ctx, proposalID, depositerAddr)
if !found {
newDeposit := Deposit{depositerAddr, depositAmount}
keeper.setDeposit(ctx, proposalID, depositerAddr, newDeposit)
} else {
currDeposit.Amount = currDeposit.Amount.Plus(depositAmount)
keeper.setDeposit(ctx, proposalID, depositerAddr, currDeposit)
}
return nil, activatedVotingPeriod
}
// Gets all the deposits on a specific proposal
func (keeper Keeper) GetDeposits(ctx sdk.Context, proposalID int64) sdk.Iterator {
store := ctx.KVStore(keeper.storeKey)
return sdk.KVStorePrefixIterator(store, KeyDepositsSubspace(proposalID))
}
// Returns and deletes all the deposits on a specific proposal
func (keeper Keeper) RefundDeposits(ctx sdk.Context, proposalID int64) {
store := ctx.KVStore(keeper.storeKey)
depositsIterator := keeper.GetDeposits(ctx, proposalID)
for ; depositsIterator.Valid(); depositsIterator.Next() {
deposit := &Deposit{}
keeper.cdc.MustUnmarshalBinary(depositsIterator.Value(), deposit)
_, _, err := keeper.ck.AddCoins(ctx, deposit.Depositer, deposit.Amount)
if err != nil {
panic("should not happen")
}
store.Delete(depositsIterator.Key())
}
depositsIterator.Close()
}
// Deletes all the deposits on a specific proposal without refunding them
func (keeper Keeper) DeleteDeposits(ctx sdk.Context, proposalID int64) {
store := ctx.KVStore(keeper.storeKey)
depositsIterator := keeper.GetDeposits(ctx, proposalID)
for ; depositsIterator.Valid(); depositsIterator.Next() {
store.Delete(depositsIterator.Key())
}
depositsIterator.Close()
}
// =====================================================
// ProposalQueues
func (keeper Keeper) getActiveProposalQueue(ctx sdk.Context) ProposalQueue {
store := ctx.KVStore(keeper.storeKey)
bz := store.Get(KeyActiveProposalQueue)
if bz == nil {
return nil
}
var proposalQueue ProposalQueue
keeper.cdc.MustUnmarshalBinary(bz, &proposalQueue)
return proposalQueue
}
func (keeper Keeper) setActiveProposalQueue(ctx sdk.Context, proposalQueue ProposalQueue) {
store := ctx.KVStore(keeper.storeKey)
bz := keeper.cdc.MustMarshalBinary(proposalQueue)
store.Set(KeyActiveProposalQueue, bz)
}
// Return the Proposal at the front of the ProposalQueue
func (keeper Keeper) ActiveProposalQueuePeek(ctx sdk.Context) Proposal {
proposalQueue := keeper.getActiveProposalQueue(ctx)
if len(proposalQueue) == 0 {
return nil
}
return keeper.GetProposal(ctx, proposalQueue[0])
}
// Remove and return a Proposal from the front of the ProposalQueue
func (keeper Keeper) ActiveProposalQueuePop(ctx sdk.Context) Proposal {
proposalQueue := keeper.getActiveProposalQueue(ctx)
if len(proposalQueue) == 0 {
return nil
}
frontElement, proposalQueue := proposalQueue[0], proposalQueue[1:]
keeper.setActiveProposalQueue(ctx, proposalQueue)
return keeper.GetProposal(ctx, frontElement)
}
// Add a proposalID to the back of the ProposalQueue
func (keeper Keeper) ActiveProposalQueuePush(ctx sdk.Context, proposal Proposal) {
proposalQueue := append(keeper.getActiveProposalQueue(ctx), proposal.GetProposalID())
keeper.setActiveProposalQueue(ctx, proposalQueue)
}
func (keeper Keeper) getInactiveProposalQueue(ctx sdk.Context) ProposalQueue {
store := ctx.KVStore(keeper.storeKey)
bz := store.Get(KeyInactiveProposalQueue)
if bz == nil {
return nil
}
var proposalQueue ProposalQueue
keeper.cdc.MustUnmarshalBinary(bz, &proposalQueue)
return proposalQueue
}
func (keeper Keeper) setInactiveProposalQueue(ctx sdk.Context, proposalQueue ProposalQueue) {
store := ctx.KVStore(keeper.storeKey)
bz := keeper.cdc.MustMarshalBinary(proposalQueue)
store.Set(KeyInactiveProposalQueue, bz)
}
// Return the Proposal at the front of the ProposalQueue
func (keeper Keeper) InactiveProposalQueuePeek(ctx sdk.Context) Proposal {
proposalQueue := keeper.getInactiveProposalQueue(ctx)
if len(proposalQueue) == 0 {
return nil
}
return keeper.GetProposal(ctx, proposalQueue[0])
}
// Remove and return a Proposal from the front of the ProposalQueue
func (keeper Keeper) InactiveProposalQueuePop(ctx sdk.Context) Proposal {
proposalQueue := keeper.getInactiveProposalQueue(ctx)
if len(proposalQueue) == 0 {
return nil
}
frontElement, proposalQueue := proposalQueue[0], proposalQueue[1:]
keeper.setInactiveProposalQueue(ctx, proposalQueue)
return keeper.GetProposal(ctx, frontElement)
}
// Add a proposalID to the back of the ProposalQueue
func (keeper Keeper) InactiveProposalQueuePush(ctx sdk.Context, proposal Proposal) {
proposalQueue := append(keeper.getInactiveProposalQueue(ctx), proposal.GetProposalID())
keeper.setInactiveProposalQueue(ctx, proposalQueue)
}

41
x/gov/keeper_keys.go Normal file
View File

@ -0,0 +1,41 @@
package gov
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// TODO remove some of these prefixes once have working multistore
// Key for getting a the next available proposalID from the store
var (
KeyNextProposalID = []byte("newProposalID")
KeyActiveProposalQueue = []byte("activeProposalQueue")
KeyInactiveProposalQueue = []byte("inactiveProposalQueue")
)
// Key for getting a specific proposal from the store
func KeyProposal(proposalID int64) []byte {
return []byte(fmt.Sprintf("proposals:%d", proposalID))
}
// Key for getting a specific deposit from the store
func KeyDeposit(proposalID int64, depositerAddr sdk.Address) []byte {
return []byte(fmt.Sprintf("deposits:%d:%d", proposalID, depositerAddr))
}
// Key for getting a specific vote from the store
func KeyVote(proposalID int64, voterAddr sdk.Address) []byte {
return []byte(fmt.Sprintf("votes:%d:%d", proposalID, voterAddr))
}
// Key for getting all deposits on a proposal from the store
func KeyDepositsSubspace(proposalID int64) []byte {
return []byte(fmt.Sprintf("deposits:%d:", proposalID))
}
// Key for getting all votes on a proposal from the store
func KeyVotesSubspace(proposalID int64) []byte {
return []byte(fmt.Sprintf("votes:%d:", proposalID))
}

247
x/gov/keeper_test.go Normal file
View File

@ -0,0 +1,247 @@
package gov
import (
"testing"
"github.com/stretchr/testify/assert"
abci "github.com/tendermint/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)
func TestGetSetProposal(t *testing.T) {
mapp, keeper, _, _, _, _ := getMockApp(t, 0)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
proposal := keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
proposalID := proposal.GetProposalID()
keeper.SetProposal(ctx, proposal)
gotProposal := keeper.GetProposal(ctx, proposalID)
assert.True(t, ProposalEqual(proposal, gotProposal))
}
func TestIncrementProposalNumber(t *testing.T) {
mapp, keeper, _, _, _, _ := getMockApp(t, 0)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
proposal6 := keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
assert.Equal(t, int64(6), proposal6.GetProposalID())
}
func TestActivateVotingPeriod(t *testing.T) {
mapp, keeper, _, _, _, _ := getMockApp(t, 0)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
proposal := keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
assert.Equal(t, int64(-1), proposal.GetVotingStartBlock())
assert.Nil(t, keeper.ActiveProposalQueuePeek(ctx))
keeper.activateVotingPeriod(ctx, proposal)
assert.Equal(t, proposal.GetVotingStartBlock(), ctx.BlockHeight())
assert.Equal(t, proposal.GetProposalID(), keeper.ActiveProposalQueuePeek(ctx).GetProposalID())
}
func TestDeposits(t *testing.T) {
mapp, keeper, _, addrs, _, _ := getMockApp(t, 2)
SortAddresses(addrs)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
proposal := keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
proposalID := proposal.GetProposalID()
fourSteak := sdk.Coins{sdk.NewCoin("steak", 4)}
fiveSteak := sdk.Coins{sdk.NewCoin("steak", 5)}
addr0Initial := keeper.ck.GetCoins(ctx, addrs[0])
addr1Initial := keeper.ck.GetCoins(ctx, addrs[1])
// assert.True(t, addr0Initial.IsEqual(sdk.Coins{sdk.NewCoin("steak", 42)}))
assert.Equal(t, sdk.Coins{sdk.NewCoin("steak", 42)}, addr0Initial)
assert.True(t, proposal.GetTotalDeposit().IsEqual(sdk.Coins{}))
// Check no deposits at beginning
deposit, found := keeper.GetDeposit(ctx, proposalID, addrs[1])
assert.False(t, found)
assert.Equal(t, keeper.GetProposal(ctx, proposalID).GetVotingStartBlock(), int64(-1))
assert.Nil(t, keeper.ActiveProposalQueuePeek(ctx))
// Check first deposit
err, votingStarted := keeper.AddDeposit(ctx, proposalID, addrs[0], fourSteak)
assert.Nil(t, err)
assert.False(t, votingStarted)
deposit, found = keeper.GetDeposit(ctx, proposalID, addrs[0])
assert.True(t, found)
assert.Equal(t, fourSteak, deposit.Amount)
assert.Equal(t, addrs[0], deposit.Depositer)
assert.Equal(t, fourSteak, keeper.GetProposal(ctx, proposalID).GetTotalDeposit())
assert.Equal(t, addr0Initial.Minus(fourSteak), keeper.ck.GetCoins(ctx, addrs[0]))
// Check a second deposit from same address
err, votingStarted = keeper.AddDeposit(ctx, proposalID, addrs[0], fiveSteak)
assert.Nil(t, err)
assert.False(t, votingStarted)
deposit, found = keeper.GetDeposit(ctx, proposalID, addrs[0])
assert.True(t, found)
assert.Equal(t, fourSteak.Plus(fiveSteak), deposit.Amount)
assert.Equal(t, addrs[0], deposit.Depositer)
assert.Equal(t, fourSteak.Plus(fiveSteak), keeper.GetProposal(ctx, proposalID).GetTotalDeposit())
assert.Equal(t, addr0Initial.Minus(fourSteak).Minus(fiveSteak), keeper.ck.GetCoins(ctx, addrs[0]))
// Check third deposit from a new address
err, votingStarted = keeper.AddDeposit(ctx, proposalID, addrs[1], fourSteak)
assert.Nil(t, err)
assert.True(t, votingStarted)
deposit, found = keeper.GetDeposit(ctx, proposalID, addrs[1])
assert.True(t, found)
assert.Equal(t, addrs[1], deposit.Depositer)
assert.Equal(t, fourSteak, deposit.Amount)
assert.Equal(t, fourSteak.Plus(fiveSteak).Plus(fourSteak), keeper.GetProposal(ctx, proposalID).GetTotalDeposit())
assert.Equal(t, addr1Initial.Minus(fourSteak), keeper.ck.GetCoins(ctx, addrs[1]))
// Check that proposal moved to voting period
assert.Equal(t, ctx.BlockHeight(), keeper.GetProposal(ctx, proposalID).GetVotingStartBlock())
assert.NotNil(t, keeper.ActiveProposalQueuePeek(ctx))
assert.Equal(t, proposalID, keeper.ActiveProposalQueuePeek(ctx).GetProposalID())
// Test deposit iterator
depositsIterator := keeper.GetDeposits(ctx, proposalID)
assert.True(t, depositsIterator.Valid())
keeper.cdc.MustUnmarshalBinary(depositsIterator.Value(), &deposit)
assert.Equal(t, addrs[0], deposit.Depositer)
assert.Equal(t, fourSteak.Plus(fiveSteak), deposit.Amount)
depositsIterator.Next()
keeper.cdc.MustUnmarshalBinary(depositsIterator.Value(), &deposit)
assert.Equal(t, addrs[1], deposit.Depositer)
assert.Equal(t, fourSteak, deposit.Amount)
depositsIterator.Next()
assert.False(t, depositsIterator.Valid())
// Test Refund Deposits
deposit, found = keeper.GetDeposit(ctx, proposalID, addrs[1])
assert.True(t, found)
assert.Equal(t, fourSteak, deposit.Amount)
keeper.RefundDeposits(ctx, proposalID)
deposit, found = keeper.GetDeposit(ctx, proposalID, addrs[1])
assert.False(t, found)
assert.Equal(t, addr0Initial, keeper.ck.GetCoins(ctx, addrs[0]))
assert.Equal(t, addr1Initial, keeper.ck.GetCoins(ctx, addrs[1]))
}
func TestVotes(t *testing.T) {
mapp, keeper, _, addrs, _, _ := getMockApp(t, 2)
SortAddresses(addrs)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
proposal := keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
proposalID := proposal.GetProposalID()
proposal.SetStatus(StatusVotingPeriod)
keeper.SetProposal(ctx, proposal)
// Test first vote
keeper.AddVote(ctx, proposalID, addrs[0], OptionAbstain)
vote, found := keeper.GetVote(ctx, proposalID, addrs[0])
assert.True(t, found)
assert.Equal(t, addrs[0], vote.Voter)
assert.Equal(t, proposalID, vote.ProposalID)
assert.Equal(t, OptionAbstain, vote.Option)
// Test change of vote
keeper.AddVote(ctx, proposalID, addrs[0], OptionYes)
vote, found = keeper.GetVote(ctx, proposalID, addrs[0])
assert.True(t, found)
assert.Equal(t, addrs[0], vote.Voter)
assert.Equal(t, proposalID, vote.ProposalID)
assert.Equal(t, OptionYes, vote.Option)
// Test second vote
keeper.AddVote(ctx, proposalID, addrs[1], OptionNoWithVeto)
vote, found = keeper.GetVote(ctx, proposalID, addrs[1])
assert.True(t, found)
assert.Equal(t, addrs[1], vote.Voter)
assert.Equal(t, proposalID, vote.ProposalID)
assert.Equal(t, OptionNoWithVeto, vote.Option)
// Test vote iterator
votesIterator := keeper.GetVotes(ctx, proposalID)
assert.True(t, votesIterator.Valid())
keeper.cdc.MustUnmarshalBinary(votesIterator.Value(), &vote)
assert.True(t, votesIterator.Valid())
assert.Equal(t, addrs[0], vote.Voter)
assert.Equal(t, proposalID, vote.ProposalID)
assert.Equal(t, OptionYes, vote.Option)
votesIterator.Next()
assert.True(t, votesIterator.Valid())
keeper.cdc.MustUnmarshalBinary(votesIterator.Value(), &vote)
assert.True(t, votesIterator.Valid())
assert.Equal(t, addrs[1], vote.Voter)
assert.Equal(t, proposalID, vote.ProposalID)
assert.Equal(t, OptionNoWithVeto, vote.Option)
votesIterator.Next()
assert.False(t, votesIterator.Valid())
}
func TestProposalQueues(t *testing.T) {
mapp, keeper, _, _, _, _ := getMockApp(t, 0)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
mapp.InitChainer(ctx, abci.RequestInitChain{})
assert.Nil(t, keeper.InactiveProposalQueuePeek(ctx))
assert.Nil(t, keeper.ActiveProposalQueuePeek(ctx))
// create test proposals
proposal := keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
proposal2 := keeper.NewTextProposal(ctx, "Test2", "description", ProposalTypeText)
proposal3 := keeper.NewTextProposal(ctx, "Test3", "description", ProposalTypeText)
proposal4 := keeper.NewTextProposal(ctx, "Test4", "description", ProposalTypeText)
// test pushing to inactive proposal queue
keeper.InactiveProposalQueuePush(ctx, proposal)
keeper.InactiveProposalQueuePush(ctx, proposal2)
keeper.InactiveProposalQueuePush(ctx, proposal3)
keeper.InactiveProposalQueuePush(ctx, proposal4)
// test peeking and popping from inactive proposal queue
assert.Equal(t, keeper.InactiveProposalQueuePeek(ctx).GetProposalID(), proposal.GetProposalID())
assert.Equal(t, keeper.InactiveProposalQueuePop(ctx).GetProposalID(), proposal.GetProposalID())
assert.Equal(t, keeper.InactiveProposalQueuePeek(ctx).GetProposalID(), proposal2.GetProposalID())
assert.Equal(t, keeper.InactiveProposalQueuePop(ctx).GetProposalID(), proposal2.GetProposalID())
assert.Equal(t, keeper.InactiveProposalQueuePeek(ctx).GetProposalID(), proposal3.GetProposalID())
assert.Equal(t, keeper.InactiveProposalQueuePop(ctx).GetProposalID(), proposal3.GetProposalID())
assert.Equal(t, keeper.InactiveProposalQueuePeek(ctx).GetProposalID(), proposal4.GetProposalID())
assert.Equal(t, keeper.InactiveProposalQueuePop(ctx).GetProposalID(), proposal4.GetProposalID())
// test pushing to active proposal queue
keeper.ActiveProposalQueuePush(ctx, proposal)
keeper.ActiveProposalQueuePush(ctx, proposal2)
keeper.ActiveProposalQueuePush(ctx, proposal3)
keeper.ActiveProposalQueuePush(ctx, proposal4)
// test peeking and popping from active proposal queue
assert.Equal(t, keeper.ActiveProposalQueuePeek(ctx).GetProposalID(), proposal.GetProposalID())
assert.Equal(t, keeper.ActiveProposalQueuePop(ctx).GetProposalID(), proposal.GetProposalID())
assert.Equal(t, keeper.ActiveProposalQueuePeek(ctx).GetProposalID(), proposal2.GetProposalID())
assert.Equal(t, keeper.ActiveProposalQueuePop(ctx).GetProposalID(), proposal2.GetProposalID())
assert.Equal(t, keeper.ActiveProposalQueuePeek(ctx).GetProposalID(), proposal3.GetProposalID())
assert.Equal(t, keeper.ActiveProposalQueuePop(ctx).GetProposalID(), proposal3.GetProposalID())
assert.Equal(t, keeper.ActiveProposalQueuePeek(ctx).GetProposalID(), proposal4.GetProposalID())
assert.Equal(t, keeper.ActiveProposalQueuePop(ctx).GetProposalID(), proposal4.GetProposalID())
}

222
x/gov/msgs.go Normal file
View File

@ -0,0 +1,222 @@
package gov
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// name to idetify transaction types
const MsgType = "gov"
//-----------------------------------------------------------
// MsgSubmitProposal
type MsgSubmitProposal struct {
Title string // Title of the proposal
Description string // Description of the proposal
ProposalType ProposalKind // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal}
Proposer sdk.Address // Address of the proposer
InitialDeposit sdk.Coins // Initial deposit paid by sender. Must be strictly positive.
}
func NewMsgSubmitProposal(title string, description string, proposalType ProposalKind, proposer sdk.Address, initialDeposit sdk.Coins) MsgSubmitProposal {
return MsgSubmitProposal{
Title: title,
Description: description,
ProposalType: proposalType,
Proposer: proposer,
InitialDeposit: initialDeposit,
}
}
// Implements Msg.
func (msg MsgSubmitProposal) Type() string { return MsgType }
// Implements Msg.
func (msg MsgSubmitProposal) ValidateBasic() sdk.Error {
if len(msg.Title) == 0 {
return ErrInvalidTitle(DefaultCodespace, msg.Title) // TODO: Proper Error
}
if len(msg.Description) == 0 {
return ErrInvalidDescription(DefaultCodespace, msg.Description) // TODO: Proper Error
}
if !validProposalType(msg.ProposalType) {
return ErrInvalidProposalType(DefaultCodespace, ProposalTypeToString(msg.ProposalType))
}
if len(msg.Proposer) == 0 {
return sdk.ErrInvalidAddress(msg.Proposer.String())
}
if !msg.InitialDeposit.IsValid() {
return sdk.ErrInvalidCoins(msg.InitialDeposit.String())
}
if !msg.InitialDeposit.IsNotNegative() {
return sdk.ErrInvalidCoins(msg.InitialDeposit.String())
}
return nil
}
func (msg MsgSubmitProposal) String() string {
return fmt.Sprintf("MsgSubmitProposal{%v, %v, %v, %v}", msg.Title, msg.Description, ProposalTypeToString(msg.ProposalType), msg.InitialDeposit)
}
// Implements Msg.
func (msg MsgSubmitProposal) Get(key interface{}) (value interface{}) {
return nil
}
// Implements Msg.
func (msg MsgSubmitProposal) GetSignBytes() []byte {
b, err := msgCdc.MarshalJSON(struct {
Title string `json:"title"`
Description string `json:"description"`
ProposalType string `json:"proposal_type"`
Proposer string `json:"proposer"`
InitialDeposit sdk.Coins `json:"deposit"`
}{
Title: msg.Title,
Description: msg.Description,
ProposalType: ProposalTypeToString(msg.ProposalType),
Proposer: sdk.MustBech32ifyVal(msg.Proposer),
InitialDeposit: msg.InitialDeposit,
})
if err != nil {
panic(err)
}
return b
}
// Implements Msg.
func (msg MsgSubmitProposal) GetSigners() []sdk.Address {
return []sdk.Address{msg.Proposer}
}
//-----------------------------------------------------------
// MsgDeposit
type MsgDeposit struct {
ProposalID int64 `json:"proposalID"` // ID of the proposal
Depositer sdk.Address `json:"depositer"` // Address of the depositer
Amount sdk.Coins `json:"amount"` // Coins to add to the proposal's deposit
}
func NewMsgDeposit(depositer sdk.Address, proposalID int64, amount sdk.Coins) MsgDeposit {
return MsgDeposit{
ProposalID: proposalID,
Depositer: depositer,
Amount: amount,
}
}
// Implements Msg.
func (msg MsgDeposit) Type() string { return MsgType }
// Implements Msg.
func (msg MsgDeposit) ValidateBasic() sdk.Error {
if len(msg.Depositer) == 0 {
return sdk.ErrInvalidAddress(msg.Depositer.String())
}
if !msg.Amount.IsValid() {
return sdk.ErrInvalidCoins(msg.Amount.String())
}
if !msg.Amount.IsNotNegative() {
return sdk.ErrInvalidCoins(msg.Amount.String())
}
if msg.ProposalID < 0 {
return ErrUnknownProposal(DefaultCodespace, msg.ProposalID)
}
return nil
}
func (msg MsgDeposit) String() string {
return fmt.Sprintf("MsgDeposit{%v=>%v: %v}", msg.Depositer, msg.ProposalID, msg.Amount)
}
// Implements Msg.
func (msg MsgDeposit) Get(key interface{}) (value interface{}) {
return nil
}
// Implements Msg.
func (msg MsgDeposit) GetSignBytes() []byte {
b, err := msgCdc.MarshalJSON(struct {
ProposalID int64 `json:"proposalID"`
Depositer string `json:"proposer"`
Amount sdk.Coins `json:"deposit"`
}{
ProposalID: msg.ProposalID,
Depositer: sdk.MustBech32ifyVal(msg.Depositer),
Amount: msg.Amount,
})
if err != nil {
panic(err)
}
return b
}
// Implements Msg.
func (msg MsgDeposit) GetSigners() []sdk.Address {
return []sdk.Address{msg.Depositer}
}
//-----------------------------------------------------------
// MsgVote
type MsgVote struct {
ProposalID int64 // proposalID of the proposal
Voter sdk.Address // address of the voter
Option VoteOption // option from OptionSet chosen by the voter
}
func NewMsgVote(voter sdk.Address, proposalID int64, option VoteOption) MsgVote {
return MsgVote{
ProposalID: proposalID,
Voter: voter,
Option: option,
}
}
// Implements Msg.
func (msg MsgVote) Type() string { return MsgType }
// Implements Msg.
func (msg MsgVote) ValidateBasic() sdk.Error {
if len(msg.Voter.Bytes()) == 0 {
return sdk.ErrInvalidAddress(msg.Voter.String())
}
if msg.ProposalID < 0 {
return ErrUnknownProposal(DefaultCodespace, msg.ProposalID)
}
if !validVoteOption(msg.Option) {
return ErrInvalidVote(DefaultCodespace, VoteOptionToString(msg.Option))
}
return nil
}
func (msg MsgVote) String() string {
return fmt.Sprintf("MsgVote{%v - %v}", msg.ProposalID, msg.Option)
}
// Implements Msg.
func (msg MsgVote) Get(key interface{}) (value interface{}) {
return nil
}
// Implements Msg.
func (msg MsgVote) GetSignBytes() []byte {
b, err := msgCdc.MarshalJSON(struct {
ProposalID int64 `json:"proposalID"`
Voter string `json:"voter"`
Option string `json:"option"`
}{
ProposalID: msg.ProposalID,
Voter: sdk.MustBech32ifyVal(msg.Voter),
Option: VoteOptionToString(msg.Option),
})
if err != nil {
panic(err)
}
return b
}
// Implements Msg.
func (msg MsgVote) GetSigners() []sdk.Address {
return []sdk.Address{msg.Voter}
}

105
x/gov/msgs_test.go Normal file
View File

@ -0,0 +1,105 @@
package gov
import (
"testing"
"github.com/stretchr/testify/assert"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth/mock"
)
var (
coinsPos = sdk.Coins{sdk.NewCoin("steak", 1000)}
coinsZero = sdk.Coins{}
coinsNeg = sdk.Coins{sdk.NewCoin("steak", -10000)}
coinsPosNotAtoms = sdk.Coins{sdk.NewCoin("foo", 10000)}
coinsMulti = sdk.Coins{sdk.NewCoin("foo", 10000), sdk.NewCoin("steak", 1000)}
)
// test ValidateBasic for MsgCreateValidator
func TestMsgSubmitProposal(t *testing.T) {
_, addrs, _, _ := mock.CreateGenAccounts(1, sdk.Coins{})
tests := []struct {
title, description string
proposalType byte
proposerAddr sdk.Address
initialDeposit sdk.Coins
expectPass bool
}{
{"Test Proposal", "the purpose of this proposal is to test", ProposalTypeText, addrs[0], coinsPos, true},
{"", "the purpose of this proposal is to test", ProposalTypeText, addrs[0], coinsPos, false},
{"Test Proposal", "", ProposalTypeText, addrs[0], coinsPos, false},
{"Test Proposal", "the purpose of this proposal is to test", ProposalTypeParameterChange, addrs[0], coinsPos, true},
{"Test Proposal", "the purpose of this proposal is to test", ProposalTypeSoftwareUpgrade, addrs[0], coinsPos, true},
{"Test Proposal", "the purpose of this proposal is to test", 0x05, addrs[0], coinsPos, false},
{"Test Proposal", "the purpose of this proposal is to test", ProposalTypeText, sdk.Address{}, coinsPos, false},
{"Test Proposal", "the purpose of this proposal is to test", ProposalTypeText, addrs[0], coinsZero, true},
{"Test Proposal", "the purpose of this proposal is to test", ProposalTypeText, addrs[0], coinsNeg, false},
{"Test Proposal", "the purpose of this proposal is to test", ProposalTypeText, addrs[0], coinsMulti, true},
}
for i, tc := range tests {
msg := NewMsgSubmitProposal(tc.title, tc.description, tc.proposalType, tc.proposerAddr, tc.initialDeposit)
if tc.expectPass {
assert.Nil(t, msg.ValidateBasic(), "test: %v", i)
} else {
assert.NotNil(t, msg.ValidateBasic(), "test: %v", i)
}
}
}
// test ValidateBasic for MsgDeposit
func TestMsgDeposit(t *testing.T) {
_, addrs, _, _ := mock.CreateGenAccounts(1, sdk.Coins{})
tests := []struct {
proposalID int64
depositerAddr sdk.Address
depositAmount sdk.Coins
expectPass bool
}{
{0, addrs[0], coinsPos, true},
{-1, addrs[0], coinsPos, false},
{1, sdk.Address{}, coinsPos, false},
{1, addrs[0], coinsZero, true},
{1, addrs[0], coinsNeg, false},
{1, addrs[0], coinsMulti, true},
}
for i, tc := range tests {
msg := NewMsgDeposit(tc.depositerAddr, tc.proposalID, tc.depositAmount)
if tc.expectPass {
assert.Nil(t, msg.ValidateBasic(), "test: %v", i)
} else {
assert.NotNil(t, msg.ValidateBasic(), "test: %v", i)
}
}
}
// test ValidateBasic for MsgDeposit
func TestMsgVote(t *testing.T) {
_, addrs, _, _ := mock.CreateGenAccounts(1, sdk.Coins{})
tests := []struct {
proposalID int64
voterAddr sdk.Address
option VoteOption
expectPass bool
}{
{0, addrs[0], OptionYes, true},
{-1, addrs[0], OptionYes, false},
{0, sdk.Address{}, OptionYes, false},
{0, addrs[0], OptionNo, true},
{0, addrs[0], OptionNoWithVeto, true},
{0, addrs[0], OptionAbstain, true},
{0, addrs[0], VoteOption(0x13), false},
}
for i, tc := range tests {
msg := NewMsgVote(tc.voterAddr, tc.proposalID, tc.option)
if tc.expectPass {
assert.Nil(t, msg.ValidateBasic(), "test: %v", i)
} else {
assert.NotNil(t, msg.ValidateBasic(), "test: %v", i)
}
}
}

23
x/gov/procedures.go Normal file
View File

@ -0,0 +1,23 @@
package gov
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Procedure around Deposits for governance
type DepositProcedure struct {
MinDeposit sdk.Coins `json:"min_deposit"` // Minimum deposit for a proposal to enter voting period.
MaxDepositPeriod int64 `json:"max_deposit_period"` // Maximum period for Atom holders to deposit on a proposal. Initial value: 2 months
}
// Procedure around Tallying votes in governance
type TallyingProcedure struct {
Threshold sdk.Rat `json:"threshold"` // Minimum propotion of Yes votes for proposal to pass. Initial value: 0.5
Veto sdk.Rat `json:"veto"` // Minimum value of Veto votes to Total votes ratio for proposal to be vetoed. Initial value: 1/3
GovernancePenalty sdk.Rat `json:"governance_penalty"` // Penalty if validator does not vote
}
// Procedure around Voting in governance
type VotingProcedure struct {
VotingPeriod int64 `json:"voting_period"` // Length of the voting period.
}

204
x/gov/proposals.go Normal file
View File

@ -0,0 +1,204 @@
package gov
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Type that represents Status as a byte
type VoteStatus = byte
// Type that represents Proposal Type as a byte
type ProposalKind = byte
//nolint
const (
StatusDepositPeriod VoteStatus = 0x01
StatusVotingPeriod VoteStatus = 0x02
StatusPassed VoteStatus = 0x03
StatusRejected VoteStatus = 0x04
ProposalTypeText ProposalKind = 0x01
ProposalTypeParameterChange ProposalKind = 0x02
ProposalTypeSoftwareUpgrade ProposalKind = 0x03
)
//-----------------------------------------------------------
// Proposal interface
type Proposal interface {
GetProposalID() int64
SetProposalID(int64)
GetTitle() string
SetTitle(string)
GetDescription() string
SetDescription(string)
GetProposalType() ProposalKind
SetProposalType(ProposalKind)
GetStatus() VoteStatus
SetStatus(VoteStatus)
GetSubmitBlock() int64
SetSubmitBlock(int64)
GetTotalDeposit() sdk.Coins
SetTotalDeposit(sdk.Coins)
GetVotingStartBlock() int64
SetVotingStartBlock(int64)
}
// checks if two proposals are equal
func ProposalEqual(proposalA Proposal, proposalB Proposal) bool {
if proposalA.GetProposalID() != proposalB.GetProposalID() ||
proposalA.GetTitle() != proposalB.GetTitle() ||
proposalA.GetDescription() != proposalB.GetDescription() ||
proposalA.GetProposalType() != proposalB.GetProposalType() ||
proposalA.GetStatus() != proposalB.GetStatus() ||
proposalA.GetSubmitBlock() != proposalB.GetSubmitBlock() ||
!(proposalA.GetTotalDeposit().IsEqual(proposalB.GetTotalDeposit())) ||
proposalA.GetVotingStartBlock() != proposalB.GetVotingStartBlock() {
return false
}
return true
}
//-----------------------------------------------------------
// Text Proposals
type TextProposal struct {
ProposalID int64 `json:"proposal_id"` // ID of the proposal
Title string `json:"title"` // Title of the proposal
Description string `json:"description"` // Description of the proposal
ProposalType ProposalKind `json:"proposal_type"` // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal}
Status VoteStatus `json:"string"` // Status of the Proposal {Pending, Active, Passed, Rejected}
SubmitBlock int64 `json:"submit_block"` // Height of the block where TxGovSubmitProposal was included
TotalDeposit sdk.Coins `json:"total_deposit"` // Current deposit on this proposal. Initial value is set at InitialDeposit
VotingStartBlock int64 `json:"voting_start_block"` // Height of the block where MinDeposit was reached. -1 if MinDeposit is not reached
}
// Implements Proposal Interface
var _ Proposal = (*TextProposal)(nil)
// nolint
func (tp TextProposal) GetProposalID() int64 { return tp.ProposalID }
func (tp *TextProposal) SetProposalID(proposalID int64) { tp.ProposalID = proposalID }
func (tp TextProposal) GetTitle() string { return tp.Title }
func (tp *TextProposal) SetTitle(title string) { tp.Title = title }
func (tp TextProposal) GetDescription() string { return tp.Description }
func (tp *TextProposal) SetDescription(description string) { tp.Description = description }
func (tp TextProposal) GetProposalType() ProposalKind { return tp.ProposalType }
func (tp *TextProposal) SetProposalType(proposalType ProposalKind) { tp.ProposalType = proposalType }
func (tp TextProposal) GetStatus() VoteStatus { return tp.Status }
func (tp *TextProposal) SetStatus(status VoteStatus) { tp.Status = status }
func (tp TextProposal) GetSubmitBlock() int64 { return tp.SubmitBlock }
func (tp *TextProposal) SetSubmitBlock(submitBlock int64) { tp.SubmitBlock = submitBlock }
func (tp TextProposal) GetTotalDeposit() sdk.Coins { return tp.TotalDeposit }
func (tp *TextProposal) SetTotalDeposit(totalDeposit sdk.Coins) { tp.TotalDeposit = totalDeposit }
func (tp TextProposal) GetVotingStartBlock() int64 { return tp.VotingStartBlock }
func (tp *TextProposal) SetVotingStartBlock(votingStartBlock int64) {
tp.VotingStartBlock = votingStartBlock
}
// Current Active Proposals
type ProposalQueue []int64
// ProposalTypeToString for pretty prints of ProposalType
func ProposalTypeToString(proposalType ProposalKind) string {
switch proposalType {
case 0x00:
return "Text"
case 0x01:
return "ParameterChange"
case 0x02:
return "SoftwareUpgrade"
default:
return ""
}
}
func validProposalType(proposalType ProposalKind) bool {
if proposalType == ProposalTypeText ||
proposalType == ProposalTypeParameterChange ||
proposalType == ProposalTypeSoftwareUpgrade {
return true
}
return false
}
// String to proposalType byte. Returns ff if invalid.
func StringToProposalType(str string) (ProposalKind, sdk.Error) {
switch str {
case "Text":
return ProposalTypeText, nil
case "ParameterChange":
return ProposalTypeParameterChange, nil
case "SoftwareUpgrade":
return ProposalTypeSoftwareUpgrade, nil
default:
return ProposalKind(0xff), ErrInvalidProposalType(DefaultCodespace, str)
}
}
// StatusToString for pretty prints of Status
func StatusToString(status VoteStatus) string {
switch status {
case StatusDepositPeriod:
return "DepositPeriod"
case StatusVotingPeriod:
return "VotingPeriod"
case StatusPassed:
return "Passed"
case StatusRejected:
return "Rejected"
default:
return ""
}
}
// StatusToString for pretty prints of Status
func StringToStatus(status string) VoteStatus {
switch status {
case "DepositPeriod":
return StatusDepositPeriod
case "VotingPeriod":
return StatusVotingPeriod
case "Passed":
return StatusPassed
case "Rejected":
return StatusRejected
default:
return VoteStatus(0xff)
}
}
//-----------------------------------------------------------
// Rest Proposals
type ProposalRest struct {
ProposalID int64 `json:"proposal_id"` // ID of the proposal
Title string `json:"title"` // Title of the proposal
Description string `json:"description"` // Description of the proposal
ProposalType string `json:"proposal_type"` // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal}
Status string `json:"string"` // Status of the Proposal {Pending, Active, Passed, Rejected}
SubmitBlock int64 `json:"submit_block"` // Height of the block where TxGovSubmitProposal was included
TotalDeposit sdk.Coins `json:"total_deposit"` // Current deposit on this proposal. Initial value is set at InitialDeposit
VotingStartBlock int64 `json:"voting_start_block"` // Height of the block where MinDeposit was reached. -1 if MinDeposit is not reached
}
// Turn any Proposal to a ProposalRest
func ProposalToRest(proposal Proposal) ProposalRest {
return ProposalRest{
ProposalID: proposal.GetProposalID(),
Title: proposal.GetTitle(),
Description: proposal.GetDescription(),
ProposalType: ProposalTypeToString(proposal.GetProposalType()),
Status: StatusToString(proposal.GetStatus()),
SubmitBlock: proposal.GetSubmitBlock(),
TotalDeposit: proposal.GetTotalDeposit(),
VotingStartBlock: proposal.GetVotingStartBlock(),
}
}

100
x/gov/tally.go Normal file
View File

@ -0,0 +1,100 @@
package gov
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// validatorGovInfo used for tallying
type validatorGovInfo struct {
Address sdk.Address // sdk.Address of the validator owner
Power sdk.Rat // Power of a Validator
DelegatorShares sdk.Rat // Total outstanding delegator shares
Minus sdk.Rat // Minus of validator, used to compute validator's voting power
Vote VoteOption // Vote of the validator
}
func tally(ctx sdk.Context, keeper Keeper, proposal Proposal) (passes bool, nonVoting []sdk.Address) {
results := make(map[VoteOption]sdk.Rat)
results[OptionYes] = sdk.ZeroRat()
results[OptionAbstain] = sdk.ZeroRat()
results[OptionNo] = sdk.ZeroRat()
results[OptionNoWithVeto] = sdk.ZeroRat()
totalVotingPower := sdk.ZeroRat()
currValidators := make(map[string]validatorGovInfo)
keeper.vs.IterateValidatorsBonded(ctx, func(index int64, validator sdk.Validator) (stop bool) {
currValidators[validator.GetOwner().String()] = validatorGovInfo{
Address: validator.GetOwner(),
Power: validator.GetPower(),
DelegatorShares: validator.GetDelegatorShares(),
Minus: sdk.ZeroRat(),
Vote: OptionEmpty,
}
return false
})
// iterate over all the votes
votesIterator := keeper.GetVotes(ctx, proposal.GetProposalID())
for ; votesIterator.Valid(); votesIterator.Next() {
vote := &Vote{}
keeper.cdc.MustUnmarshalBinary(votesIterator.Value(), vote)
// if validator, just record it in the map
// if delegator tally voting power
if val, ok := currValidators[vote.Voter.String()]; ok {
val.Vote = vote.Option
currValidators[vote.Voter.String()] = val
} else {
keeper.ds.IterateDelegations(ctx, vote.Voter, func(index int64, delegation sdk.Delegation) (stop bool) {
val := currValidators[delegation.GetValidator().String()]
val.Minus = val.Minus.Add(delegation.GetBondShares())
currValidators[delegation.GetValidator().String()] = val
delegatorShare := delegation.GetBondShares().Quo(val.DelegatorShares)
votingPower := val.Power.Mul(delegatorShare)
results[vote.Option] = results[vote.Option].Add(votingPower)
totalVotingPower = totalVotingPower.Add(votingPower)
return false
})
}
keeper.deleteVote(ctx, vote.ProposalID, vote.Voter)
}
votesIterator.Close()
// Iterate over the validators again to tally their voting power and see who didn't vote
nonVoting = []sdk.Address{}
for _, val := range currValidators {
if val.Vote == OptionEmpty {
nonVoting = append(nonVoting, val.Address)
continue
}
sharesAfterMinus := val.DelegatorShares.Sub(val.Minus)
percentAfterMinus := sharesAfterMinus.Quo(val.DelegatorShares)
votingPower := val.Power.Mul(percentAfterMinus)
results[val.Vote] = results[val.Vote].Add(votingPower)
totalVotingPower = totalVotingPower.Add(votingPower)
}
tallyingProcedure := keeper.GetTallyingProcedure(ctx)
// If no one votes, proposal fails
if totalVotingPower.Sub(results[OptionAbstain]).Equal(sdk.ZeroRat()) {
return false, nonVoting
}
// If more than 1/3 of voters veto, proposal fails
if results[OptionNoWithVeto].Quo(totalVotingPower).GT(tallyingProcedure.Veto) {
return false, nonVoting
}
// If more than 1/2 of non-abstaining voters vote Yes, proposal passes
if results[OptionYes].Quo(totalVotingPower.Sub(results[OptionAbstain])).GT(tallyingProcedure.Threshold) {
return true, nonVoting
}
// If more than 1/2 of non-abstaining voters vote No, proposal fails
return false, nonVoting
}

391
x/gov/tally_test.go Normal file
View File

@ -0,0 +1,391 @@
package gov
import (
"testing"
"github.com/stretchr/testify/assert"
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/abci/types"
crypto "github.com/tendermint/go-crypto"
"github.com/cosmos/cosmos-sdk/x/stake"
)
func TestTallyNoOneVotes(t *testing.T) {
mapp, keeper, sk, addrs, _, _ := getMockApp(t, 10)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
stakeHandler := stake.NewHandler(sk)
dummyDescription := stake.NewDescription("T", "E", "S", "T")
val1CreateMsg := stake.NewMsgCreateValidator(addrs[0], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 5), dummyDescription)
stakeHandler(ctx, val1CreateMsg)
val2CreateMsg := stake.NewMsgCreateValidator(addrs[1], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 5), dummyDescription)
stakeHandler(ctx, val2CreateMsg)
proposal := keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
proposalID := proposal.GetProposalID()
proposal.SetStatus(StatusVotingPeriod)
keeper.SetProposal(ctx, proposal)
passes, _ := tally(ctx, keeper, keeper.GetProposal(ctx, proposalID))
assert.False(t, passes)
}
func TestTallyOnlyValidatorsAllYes(t *testing.T) {
mapp, keeper, sk, addrs, _, _ := getMockApp(t, 10)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
stakeHandler := stake.NewHandler(sk)
dummyDescription := stake.NewDescription("T", "E", "S", "T")
val1CreateMsg := stake.NewMsgCreateValidator(addrs[0], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 5), dummyDescription)
res := stakeHandler(ctx, val1CreateMsg)
assert.True(t, res.IsOK())
val2CreateMsg := stake.NewMsgCreateValidator(addrs[1], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 5), dummyDescription)
res = stakeHandler(ctx, val2CreateMsg)
assert.True(t, res.IsOK())
proposal := keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
proposalID := proposal.GetProposalID()
proposal.SetStatus(StatusVotingPeriod)
keeper.SetProposal(ctx, proposal)
err := keeper.AddVote(ctx, proposalID, addrs[0], OptionYes)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[1], OptionYes)
assert.Nil(t, err)
passes, _ := tally(ctx, keeper, keeper.GetProposal(ctx, proposalID))
assert.True(t, passes)
}
func TestTallyOnlyValidators51No(t *testing.T) {
mapp, keeper, sk, addrs, _, _ := getMockApp(t, 10)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
stakeHandler := stake.NewHandler(sk)
dummyDescription := stake.NewDescription("T", "E", "S", "T")
val1CreateMsg := stake.NewMsgCreateValidator(addrs[0], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 5), dummyDescription)
stakeHandler(ctx, val1CreateMsg)
val2CreateMsg := stake.NewMsgCreateValidator(addrs[1], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 6), dummyDescription)
stakeHandler(ctx, val2CreateMsg)
proposal := keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
proposalID := proposal.GetProposalID()
proposal.SetStatus(StatusVotingPeriod)
keeper.SetProposal(ctx, proposal)
err := keeper.AddVote(ctx, proposalID, addrs[0], OptionYes)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[1], OptionNo)
assert.Nil(t, err)
passes, _ := tally(ctx, keeper, keeper.GetProposal(ctx, proposalID))
assert.False(t, passes)
}
func TestTallyOnlyValidators51Yes(t *testing.T) {
mapp, keeper, sk, addrs, _, _ := getMockApp(t, 10)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
stakeHandler := stake.NewHandler(sk)
dummyDescription := stake.NewDescription("T", "E", "S", "T")
val1CreateMsg := stake.NewMsgCreateValidator(addrs[0], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 6), dummyDescription)
stakeHandler(ctx, val1CreateMsg)
val2CreateMsg := stake.NewMsgCreateValidator(addrs[1], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 6), dummyDescription)
stakeHandler(ctx, val2CreateMsg)
val3CreateMsg := stake.NewMsgCreateValidator(addrs[2], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 7), dummyDescription)
stakeHandler(ctx, val3CreateMsg)
proposal := keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
proposalID := proposal.GetProposalID()
proposal.SetStatus(StatusVotingPeriod)
keeper.SetProposal(ctx, proposal)
err := keeper.AddVote(ctx, proposalID, addrs[0], OptionYes)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[1], OptionYes)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[2], OptionNo)
assert.Nil(t, err)
passes, _ := tally(ctx, keeper, keeper.GetProposal(ctx, proposalID))
assert.True(t, passes)
}
func TestTallyOnlyValidatorsVetoed(t *testing.T) {
mapp, keeper, sk, addrs, _, _ := getMockApp(t, 10)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
stakeHandler := stake.NewHandler(sk)
dummyDescription := stake.NewDescription("T", "E", "S", "T")
val1CreateMsg := stake.NewMsgCreateValidator(addrs[0], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 6), dummyDescription)
stakeHandler(ctx, val1CreateMsg)
val2CreateMsg := stake.NewMsgCreateValidator(addrs[1], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 6), dummyDescription)
stakeHandler(ctx, val2CreateMsg)
val3CreateMsg := stake.NewMsgCreateValidator(addrs[2], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 7), dummyDescription)
stakeHandler(ctx, val3CreateMsg)
proposal := keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
proposalID := proposal.GetProposalID()
proposal.SetStatus(StatusVotingPeriod)
keeper.SetProposal(ctx, proposal)
err := keeper.AddVote(ctx, proposalID, addrs[0], OptionYes)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[1], OptionYes)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[2], OptionNoWithVeto)
assert.Nil(t, err)
passes, _ := tally(ctx, keeper, keeper.GetProposal(ctx, proposalID))
assert.False(t, passes)
}
func TestTallyOnlyValidatorsAbstainPasses(t *testing.T) {
mapp, keeper, sk, addrs, _, _ := getMockApp(t, 10)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
stakeHandler := stake.NewHandler(sk)
dummyDescription := stake.NewDescription("T", "E", "S", "T")
val1CreateMsg := stake.NewMsgCreateValidator(addrs[0], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 6), dummyDescription)
stakeHandler(ctx, val1CreateMsg)
val2CreateMsg := stake.NewMsgCreateValidator(addrs[1], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 6), dummyDescription)
stakeHandler(ctx, val2CreateMsg)
val3CreateMsg := stake.NewMsgCreateValidator(addrs[2], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 7), dummyDescription)
stakeHandler(ctx, val3CreateMsg)
proposal := keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
proposalID := proposal.GetProposalID()
proposal.SetStatus(StatusVotingPeriod)
keeper.SetProposal(ctx, proposal)
err := keeper.AddVote(ctx, proposalID, addrs[0], OptionAbstain)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[1], OptionNo)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[2], OptionYes)
assert.Nil(t, err)
passes, _ := tally(ctx, keeper, keeper.GetProposal(ctx, proposalID))
assert.True(t, passes)
}
func TestTallyOnlyValidatorsAbstainFails(t *testing.T) {
mapp, keeper, sk, addrs, _, _ := getMockApp(t, 10)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
stakeHandler := stake.NewHandler(sk)
dummyDescription := stake.NewDescription("T", "E", "S", "T")
val1CreateMsg := stake.NewMsgCreateValidator(addrs[0], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 6), dummyDescription)
stakeHandler(ctx, val1CreateMsg)
val2CreateMsg := stake.NewMsgCreateValidator(addrs[1], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 6), dummyDescription)
stakeHandler(ctx, val2CreateMsg)
val3CreateMsg := stake.NewMsgCreateValidator(addrs[2], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 7), dummyDescription)
stakeHandler(ctx, val3CreateMsg)
proposal := keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
proposalID := proposal.GetProposalID()
proposal.SetStatus(StatusVotingPeriod)
keeper.SetProposal(ctx, proposal)
err := keeper.AddVote(ctx, proposalID, addrs[0], OptionAbstain)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[1], OptionYes)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[2], OptionNo)
assert.Nil(t, err)
passes, _ := tally(ctx, keeper, keeper.GetProposal(ctx, proposalID))
assert.False(t, passes)
}
func TestTallyOnlyValidatorsNonVoter(t *testing.T) {
mapp, keeper, sk, addrs, _, _ := getMockApp(t, 10)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
stakeHandler := stake.NewHandler(sk)
dummyDescription := stake.NewDescription("T", "E", "S", "T")
val1CreateMsg := stake.NewMsgCreateValidator(addrs[0], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 6), dummyDescription)
stakeHandler(ctx, val1CreateMsg)
val2CreateMsg := stake.NewMsgCreateValidator(addrs[1], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 6), dummyDescription)
stakeHandler(ctx, val2CreateMsg)
val3CreateMsg := stake.NewMsgCreateValidator(addrs[2], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 7), dummyDescription)
stakeHandler(ctx, val3CreateMsg)
proposal := keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
proposalID := proposal.GetProposalID()
proposal.SetStatus(StatusVotingPeriod)
keeper.SetProposal(ctx, proposal)
err := keeper.AddVote(ctx, proposalID, addrs[1], OptionYes)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[2], OptionNo)
assert.Nil(t, err)
passes, nonVoting := tally(ctx, keeper, keeper.GetProposal(ctx, proposalID))
assert.False(t, passes)
assert.Equal(t, 1, len(nonVoting))
assert.Equal(t, addrs[0], nonVoting[0])
}
func TestTallyDelgatorOverride(t *testing.T) {
mapp, keeper, sk, addrs, _, _ := getMockApp(t, 10)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
stakeHandler := stake.NewHandler(sk)
dummyDescription := stake.NewDescription("T", "E", "S", "T")
val1CreateMsg := stake.NewMsgCreateValidator(addrs[0], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 5), dummyDescription)
stakeHandler(ctx, val1CreateMsg)
val2CreateMsg := stake.NewMsgCreateValidator(addrs[1], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 6), dummyDescription)
stakeHandler(ctx, val2CreateMsg)
val3CreateMsg := stake.NewMsgCreateValidator(addrs[2], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 7), dummyDescription)
stakeHandler(ctx, val3CreateMsg)
delegator1Msg := stake.NewMsgDelegate(addrs[3], addrs[2], sdk.NewCoin("steak", 30))
stakeHandler(ctx, delegator1Msg)
proposal := keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
proposalID := proposal.GetProposalID()
proposal.SetStatus(StatusVotingPeriod)
keeper.SetProposal(ctx, proposal)
err := keeper.AddVote(ctx, proposalID, addrs[0], OptionYes)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[1], OptionYes)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[2], OptionYes)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[3], OptionNo)
assert.Nil(t, err)
passes, _ := tally(ctx, keeper, keeper.GetProposal(ctx, proposalID))
assert.False(t, passes)
}
func TestTallyDelgatorInherit(t *testing.T) {
mapp, keeper, sk, addrs, _, _ := getMockApp(t, 10)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
stakeHandler := stake.NewHandler(sk)
dummyDescription := stake.NewDescription("T", "E", "S", "T")
val1CreateMsg := stake.NewMsgCreateValidator(addrs[0], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 5), dummyDescription)
stakeHandler(ctx, val1CreateMsg)
val2CreateMsg := stake.NewMsgCreateValidator(addrs[1], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 6), dummyDescription)
stakeHandler(ctx, val2CreateMsg)
val3CreateMsg := stake.NewMsgCreateValidator(addrs[2], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 7), dummyDescription)
stakeHandler(ctx, val3CreateMsg)
delegator1Msg := stake.NewMsgDelegate(addrs[3], addrs[2], sdk.NewCoin("steak", 30))
stakeHandler(ctx, delegator1Msg)
proposal := keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
proposalID := proposal.GetProposalID()
proposal.SetStatus(StatusVotingPeriod)
keeper.SetProposal(ctx, proposal)
err := keeper.AddVote(ctx, proposalID, addrs[0], OptionNo)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[1], OptionNo)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[2], OptionYes)
assert.Nil(t, err)
passes, nonVoting := tally(ctx, keeper, keeper.GetProposal(ctx, proposalID))
assert.True(t, passes)
assert.Equal(t, 0, len(nonVoting))
}
func TestTallyDelgatorMultipleOverride(t *testing.T) {
mapp, keeper, sk, addrs, _, _ := getMockApp(t, 10)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
stakeHandler := stake.NewHandler(sk)
dummyDescription := stake.NewDescription("T", "E", "S", "T")
val1CreateMsg := stake.NewMsgCreateValidator(addrs[0], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 5), dummyDescription)
stakeHandler(ctx, val1CreateMsg)
val2CreateMsg := stake.NewMsgCreateValidator(addrs[1], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 6), dummyDescription)
stakeHandler(ctx, val2CreateMsg)
val3CreateMsg := stake.NewMsgCreateValidator(addrs[2], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 7), dummyDescription)
stakeHandler(ctx, val3CreateMsg)
delegator1Msg := stake.NewMsgDelegate(addrs[3], addrs[2], sdk.NewCoin("steak", 10))
stakeHandler(ctx, delegator1Msg)
delegator1Msg2 := stake.NewMsgDelegate(addrs[3], addrs[1], sdk.NewCoin("steak", 10))
stakeHandler(ctx, delegator1Msg2)
proposal := keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
proposalID := proposal.GetProposalID()
proposal.SetStatus(StatusVotingPeriod)
keeper.SetProposal(ctx, proposal)
err := keeper.AddVote(ctx, proposalID, addrs[0], OptionYes)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[1], OptionYes)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[2], OptionYes)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[3], OptionNo)
assert.Nil(t, err)
passes, _ := tally(ctx, keeper, keeper.GetProposal(ctx, proposalID))
assert.False(t, passes)
}
func TestTallyDelgatorMultipleInherit(t *testing.T) {
mapp, keeper, sk, addrs, _, _ := getMockApp(t, 10)
mapp.BeginBlock(abci.RequestBeginBlock{})
ctx := mapp.BaseApp.NewContext(false, abci.Header{})
stakeHandler := stake.NewHandler(sk)
dummyDescription := stake.NewDescription("T", "E", "S", "T")
val1CreateMsg := stake.NewMsgCreateValidator(addrs[0], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 25), dummyDescription)
stakeHandler(ctx, val1CreateMsg)
val2CreateMsg := stake.NewMsgCreateValidator(addrs[1], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 6), dummyDescription)
stakeHandler(ctx, val2CreateMsg)
val3CreateMsg := stake.NewMsgCreateValidator(addrs[2], crypto.GenPrivKeyEd25519().PubKey(), sdk.NewCoin("steak", 7), dummyDescription)
stakeHandler(ctx, val3CreateMsg)
delegator1Msg := stake.NewMsgDelegate(addrs[3], addrs[2], sdk.NewCoin("steak", 10))
stakeHandler(ctx, delegator1Msg)
delegator1Msg2 := stake.NewMsgDelegate(addrs[3], addrs[1], sdk.NewCoin("steak", 10))
stakeHandler(ctx, delegator1Msg2)
proposal := keeper.NewTextProposal(ctx, "Test", "description", ProposalTypeText)
proposalID := proposal.GetProposalID()
proposal.SetStatus(StatusVotingPeriod)
keeper.SetProposal(ctx, proposal)
err := keeper.AddVote(ctx, proposalID, addrs[0], OptionYes)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[1], OptionNo)
assert.Nil(t, err)
err = keeper.AddVote(ctx, proposalID, addrs[2], OptionNo)
assert.Nil(t, err)
passes, _ := tally(ctx, keeper, keeper.GetProposal(ctx, proposalID))
assert.False(t, passes)
}

107
x/gov/test_common.go Normal file
View File

@ -0,0 +1,107 @@
package gov
import (
"bytes"
"log"
"sort"
"testing"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/abci/types"
crypto "github.com/tendermint/go-crypto"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth/mock"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/cosmos-sdk/x/stake"
)
// initialize the mock application for this module
func getMockApp(t *testing.T, numGenAccs int64) (*mock.App, Keeper, stake.Keeper, []sdk.Address, []crypto.PubKey, []crypto.PrivKey) {
mapp := mock.NewApp()
stake.RegisterWire(mapp.Cdc)
RegisterWire(mapp.Cdc)
keyStake := sdk.NewKVStoreKey("stake")
keyGov := sdk.NewKVStoreKey("gov")
ck := bank.NewKeeper(mapp.AccountMapper)
sk := stake.NewKeeper(mapp.Cdc, keyStake, ck, mapp.RegisterCodespace(stake.DefaultCodespace))
keeper := NewKeeper(mapp.Cdc, keyGov, ck, sk, DefaultCodespace)
mapp.Router().AddRoute("gov", NewHandler(keeper))
require.NoError(t, mapp.CompleteSetup([]*sdk.KVStoreKey{keyStake, keyGov}))
mapp.SetEndBlocker(getEndBlocker(keeper))
mapp.SetInitChainer(getInitChainer(mapp, keeper, sk))
genAccs, addrs, pubKeys, privKeys := mock.CreateGenAccounts(numGenAccs, sdk.Coins{sdk.NewCoin("steak", 42)})
mock.SetGenesis(mapp, genAccs)
return mapp, keeper, sk, addrs, pubKeys, privKeys
}
// gov and stake endblocker
func getEndBlocker(keeper Keeper) sdk.EndBlocker {
return func(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock {
tags, _ := EndBlocker(ctx, keeper)
return abci.ResponseEndBlock{
Tags: tags,
}
}
}
// gov and stake initchainer
func getInitChainer(mapp *mock.App, keeper Keeper, stakeKeeper stake.Keeper) sdk.InitChainer {
return func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain {
mapp.InitChainer(ctx, req)
stake.InitGenesis(ctx, stakeKeeper, stake.DefaultGenesisState())
InitGenesis(ctx, keeper, DefaultGenesisState())
return abci.ResponseInitChain{}
}
}
// Sorts Addresses
func SortAddresses(addrs []sdk.Address) {
var byteAddrs [][]byte
for _, addr := range addrs {
byteAddrs = append(byteAddrs, addr.Bytes())
}
SortByteArrays(byteAddrs)
for i, byteAddr := range byteAddrs {
addrs[i] = byteAddr
}
}
// implement `Interface` in sort package.
type sortByteArrays [][]byte
func (b sortByteArrays) Len() int {
return len(b)
}
func (b sortByteArrays) Less(i, j int) bool {
// bytes package already implements Comparable for []byte.
switch bytes.Compare(b[i], b[j]) {
case -1:
return true
case 0, 1:
return false
default:
log.Panic("not fail-able with `bytes.Comparable` bounded [-1, 1].")
return false
}
}
func (b sortByteArrays) Swap(i, j int) {
b[j], b[i] = b[i], b[j]
}
// Public
func SortByteArrays(src [][]byte) [][]byte {
sorted := sortByteArrays(src)
sort.Sort(sorted)
return sorted
}

18
x/gov/wire.go Normal file
View File

@ -0,0 +1,18 @@
package gov
import (
"github.com/cosmos/cosmos-sdk/wire"
)
// Register concrete types on wire codec
func RegisterWire(cdc *wire.Codec) {
cdc.RegisterConcrete(MsgSubmitProposal{}, "cosmos-sdk/MsgSubmitProposal", nil)
cdc.RegisterConcrete(MsgDeposit{}, "cosmos-sdk/MsgDeposit", nil)
cdc.RegisterConcrete(MsgVote{}, "cosmos-sdk/MsgVote", nil)
cdc.RegisterInterface((*Proposal)(nil), nil)
cdc.RegisterConcrete(&TextProposal{}, "gov/TextProposal", nil)
}
var msgCdc = wire.NewCodec()

View File

@ -618,6 +618,13 @@ func (k Keeper) setNewParams(ctx sdk.Context, params Params) {
store.Set(ParamKey, b)
}
// Public version of setNewParams
func (k Keeper) SetNewParams(ctx sdk.Context, params Params) {
store := ctx.KVStore(k.storeKey)
b := k.cdc.MustMarshalBinary(params)
store.Set(ParamKey, b)
}
func (k Keeper) setParams(ctx sdk.Context, params Params) {
store := ctx.KVStore(k.storeKey)
exParams := k.getParams(store)
@ -652,6 +659,13 @@ func (k Keeper) setPool(ctx sdk.Context, pool Pool) {
store.Set(PoolKey, b)
}
// Public version of setpool
func (k Keeper) SetPool(ctx sdk.Context, pool Pool) {
store := ctx.KVStore(k.storeKey)
b := k.cdc.MustMarshalBinary(pool)
store.Set(PoolKey, b)
}
//__________________________________________________________________________
// get the current in-block validator operation counter
@ -777,8 +791,13 @@ func (k Keeper) Delegation(ctx sdk.Context, addrDel sdk.Address, addrVal sdk.Add
return bond
}
// Returns self as it is both a validatorset and delegationset
func (k Keeper) GetValidatorSet() sdk.ValidatorSet {
return k
}
// iterate through the active validator set and perform the provided function
func (k Keeper) IterateDelegators(ctx sdk.Context, delAddr sdk.Address, fn func(index int64, delegation sdk.Delegation) (stop bool)) {
func (k Keeper) IterateDelegations(ctx sdk.Context, delAddr sdk.Address, fn func(index int64, delegation sdk.Delegation) (stop bool)) {
store := ctx.KVStore(k.storeKey)
key := GetDelegationsKey(delAddr, k.cdc)
iterator := sdk.KVStorePrefixIterator(store, key)

View File

@ -251,12 +251,13 @@ func (v Validator) DelegatorShareExRate(pool Pool) sdk.Rat {
var _ sdk.Validator = Validator{}
// nolint - for sdk.Validator
func (v Validator) GetMoniker() string { return v.Description.Moniker }
func (v Validator) GetStatus() sdk.BondStatus { return v.Status() }
func (v Validator) GetOwner() sdk.Address { return v.Owner }
func (v Validator) GetPubKey() crypto.PubKey { return v.PubKey }
func (v Validator) GetPower() sdk.Rat { return v.PoolShares.Bonded() }
func (v Validator) GetBondHeight() int64 { return v.BondHeight }
func (v Validator) GetMoniker() string { return v.Description.Moniker }
func (v Validator) GetStatus() sdk.BondStatus { return v.Status() }
func (v Validator) GetOwner() sdk.Address { return v.Owner }
func (v Validator) GetPubKey() crypto.PubKey { return v.PubKey }
func (v Validator) GetPower() sdk.Rat { return v.PoolShares.Bonded() }
func (v Validator) GetDelegatorShares() sdk.Rat { return v.DelegatorShares }
func (v Validator) GetBondHeight() int64 { return v.BondHeight }
//Human Friendly pretty printer
func (v Validator) HumanReadableString() (string, error) {