diff --git a/.gitignore b/.gitignore index f528ccc8f..eb10faca7 100644 --- a/.gitignore +++ b/.gitignore @@ -28,5 +28,9 @@ profile.out *.log vagrant +# IDE +.idea/ +*.iml + # Graphviz dependency-graph.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 21a93b724..22e642888 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index c3bec2f8b..1c8f872fc 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -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 +} diff --git a/client/lcd/root.go b/client/lcd/root.go index 4af034297..96e8504dd 100644 --- a/client/lcd/root.go +++ b/client/lcd/root.go @@ -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 } diff --git a/cmd/gaia/app/app.go b/cmd/gaia/app/app.go index 627eee763..67aea321f 100644 --- a/cmd/gaia/app/app.go +++ b/cmd/gaia/app/app.go @@ -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{} } diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index 919a4c5b6..70d9f5564 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -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 +} diff --git a/cmd/gaia/cmd/gaiacli/main.go b/cmd/gaia/cmd/gaiacli/main.go index 2e24842e3..ad7fbf985 100644 --- a/cmd/gaia/cmd/gaiacli/main.go +++ b/cmd/gaia/cmd/gaiacli/main.go @@ -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( diff --git a/docs/spec/governance/state.md b/docs/spec/governance/state.md index 91cfa1f76..d538c765c 100644 --- a/docs/spec/governance/state.md +++ b/docs/spec/governance/state.md @@ -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 diff --git a/docs/spec/governance/transactions.md b/docs/spec/governance/transactions.md index aacf046c1..9f93b2636 100644 --- a/docs/spec/governance/transactions.md +++ b/docs/spec/governance/transactions.md @@ -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, ::) + if (validator == nil) then + + // Throws if + // ValidatorAddress is not the address of a current validator + + throw + + else + option = load(Options, ::) if (option != nil) // sender has already voted with the Atoms bonded to Address diff --git a/types/stake.go b/types/stake.go index 7484295cc..d49fe387f 100644 --- a/types/stake.go +++ b/types/stake.go @@ -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)) } diff --git a/x/auth/mock/app.go b/x/auth/mock/app.go index 8223733e0..f53243209 100644 --- a/x/auth/mock/app.go +++ b/x/auth/mock/app.go @@ -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 +} diff --git a/x/gov/client/cli/tx.go b/x/gov/client/cli/tx.go new file mode 100644 index 000000000..00933284e --- /dev/null +++ b/x/gov/client/cli/tx.go @@ -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 +} diff --git a/x/gov/client/rest/rest.go b/x/gov/client/rest/rest.go new file mode 100644 index 000000000..7b53e5bf1 --- /dev/null +++ b/x/gov/client/rest/rest.go @@ -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) + } +} diff --git a/x/gov/client/rest/util.go b/x/gov/client/rest/util.go new file mode 100644 index 000000000..15af9038a --- /dev/null +++ b/x/gov/client/rest/util.go @@ -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) +} diff --git a/x/gov/depositsvotes.go b/x/gov/depositsvotes.go new file mode 100644 index 000000000..bf36e3b0c --- /dev/null +++ b/x/gov/depositsvotes.go @@ -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), + } +} diff --git a/x/gov/endblocker_test.go b/x/gov/endblocker_test.go new file mode 100644 index 000000000..96aa1e5e4 --- /dev/null +++ b/x/gov/endblocker_test.go @@ -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()) +} diff --git a/x/gov/errors.go b/x/gov/errors.go new file mode 100644 index 000000000..9a389dc9c --- /dev/null +++ b/x/gov/errors.go @@ -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) +} diff --git a/x/gov/genesis.go b/x/gov/genesis.go new file mode 100644 index 000000000..bf0b3a155 --- /dev/null +++ b/x/gov/genesis.go @@ -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, + } +} diff --git a/x/gov/handler.go b/x/gov/handler.go new file mode 100644 index 000000000..1d9545948 --- /dev/null +++ b/x/gov/handler.go @@ -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 +} diff --git a/x/gov/keeper.go b/x/gov/keeper.go new file mode 100644 index 000000000..d0ce12982 --- /dev/null +++ b/x/gov/keeper.go @@ -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) +} diff --git a/x/gov/keeper_keys.go b/x/gov/keeper_keys.go new file mode 100644 index 000000000..8150c2b16 --- /dev/null +++ b/x/gov/keeper_keys.go @@ -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)) +} diff --git a/x/gov/keeper_test.go b/x/gov/keeper_test.go new file mode 100644 index 000000000..fc8a632cc --- /dev/null +++ b/x/gov/keeper_test.go @@ -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()) +} diff --git a/x/gov/msgs.go b/x/gov/msgs.go new file mode 100644 index 000000000..f436d024b --- /dev/null +++ b/x/gov/msgs.go @@ -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} +} diff --git a/x/gov/msgs_test.go b/x/gov/msgs_test.go new file mode 100644 index 000000000..3100f98ca --- /dev/null +++ b/x/gov/msgs_test.go @@ -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) + } + } +} diff --git a/x/gov/procedures.go b/x/gov/procedures.go new file mode 100644 index 000000000..f46c2149f --- /dev/null +++ b/x/gov/procedures.go @@ -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. +} diff --git a/x/gov/proposals.go b/x/gov/proposals.go new file mode 100644 index 000000000..c81d61e21 --- /dev/null +++ b/x/gov/proposals.go @@ -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(), + } +} diff --git a/x/gov/tally.go b/x/gov/tally.go new file mode 100644 index 000000000..2e70ac24c --- /dev/null +++ b/x/gov/tally.go @@ -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 +} diff --git a/x/gov/tally_test.go b/x/gov/tally_test.go new file mode 100644 index 000000000..ca5919dc2 --- /dev/null +++ b/x/gov/tally_test.go @@ -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) +} diff --git a/x/gov/test_common.go b/x/gov/test_common.go new file mode 100644 index 000000000..bce8e4b30 --- /dev/null +++ b/x/gov/test_common.go @@ -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 +} diff --git a/x/gov/wire.go b/x/gov/wire.go new file mode 100644 index 000000000..405ee464e --- /dev/null +++ b/x/gov/wire.go @@ -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() diff --git a/x/stake/keeper.go b/x/stake/keeper.go index cfe685304..52ed82713 100644 --- a/x/stake/keeper.go +++ b/x/stake/keeper.go @@ -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) diff --git a/x/stake/validator.go b/x/stake/validator.go index 612b0e329..e21b8f237 100644 --- a/x/stake/validator.go +++ b/x/stake/validator.go @@ -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) {