From 5735075b0526a833e46550221752614c66b2f0b2 Mon Sep 17 00:00:00 2001 From: gamarin Date: Mon, 4 Jun 2018 17:20:07 +0200 Subject: [PATCH 1/8] Revamp gov spec --- docs/spec/governance/state.md | 145 +++++++++--------- docs/spec/governance/transactions.md | 210 +++++++-------------------- 2 files changed, 122 insertions(+), 233 deletions(-) diff --git a/docs/spec/governance/state.md b/docs/spec/governance/state.md index e533ec6fe1..d82dd92acd 100644 --- a/docs/spec/governance/state.md +++ b/docs/spec/governance/state.md @@ -11,32 +11,38 @@ has to be created and the previous one rendered inactive. ```go -type VoteType byte +type Vote byte const ( - VoteTypeYes = 0x1 - VoteTypeNo = 0x2 - VoteTypeNoWithVeto = 0x3 - VoteTypeAbstain = 0x4 + VoteYes = 0x1 + VoteNo = 0x2 + VoteNoWithVeto = 0x3 + VoteAbstain = 0x4 ) type ProposalType byte const ( - ProposalTypePlainText = 0x1 + ProposalTypePlainText = 0x1 ProposalTypeSoftwareUpgrade = 0x2 +) +type ProposalStatus byte + +const ( + ProposalStatusOpen = 0x1 // Proposal is submitted. Participants can deposit on it but not vote + ProposalStatusActive = 0x2 // MinDeposit is reachhed, participants can vote + ProposalStatusAccepted = 0x3 // Proposal has been accepted + ProposalStatusRejected = 0x4 // Proposal has been rejected ) 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. + MinDeposit sdk.Coins // Minimum deposit for a proposal to enter voting period. 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 - GovernancePenalty int64 // Penalty if validator does not vote + GovernancePenalty sdk.Rat // Penalty if validator does not vote IsActive bool // If true, procedure is active. Only one procedure can have isActive true. } @@ -53,18 +59,17 @@ The current active procedure is stored in a global `params` KVStore. } ``` -### Votes +### ValidatorGovInfo + +This type is used in a temp map when tallying ```go - type Votes struct { - Yes int64 - No int64 - NoWithVeto int64 - Abstain int64 + type ValidatorGovInfo struct { + Minus sdk.Rat + Vote Vote } ``` - ### Proposals `Proposals` are an item to be voted on. @@ -77,37 +82,34 @@ type Proposal struct { TotalDeposit sdk.Coins // Current deposit on this proposal. Initial value is set at InitialDeposit Deposits []Deposit // List of deposits on the proposal SubmitBlock int64 // Height of the block where TxGovSubmitProposal was included - Submitter crypto.Address // Address of the submitter + Submitter sdk.Address // Address of the submitter VotingStartBlock int64 // Height of the block where MinDeposit was reached. -1 if MinDeposit is not reached - InitTotalVotingPower int64 // Total voting power when proposal enters voting period (default 0) InitProcedure Procedure // Active Procedure when proposal enters voting period + CurrentStatus ProposalStatus // Current status of the proposal - Votes Votes // Total votes for each option + YesVotes sdk.Rat + NoVotes sdk.Rat + NoWithVetoVotes sdk.Rat + AbstainVotes sdk.Rat } ``` -We also introduce a type `ValidatorGovInfo` +We also mention a method to update the tally for a given proposal: ```go -type ValidatorGovInfo struct { - InitVotingPower int64 // Voting power of validator when proposal enters voting period - Minus int64 // Minus of validator, used to compute validator's voting power -} + func (proposal Proposal) updateTally(vote byte, amount sdk.Rat) ``` ### Stores -*Stores are KVStores in the multistore. The key to find the store is the first parameter in the list* +*Stores are KVStores in the multistore. The key to find the store is the first parameter in the list*` +We will use one KVStore `Governance` to store two mappings: + +* A mapping from `proposalID` to `Proposal` +* A mapping from `proposalID:addresses:address` to `Vote`. This mapping allows us to query all addresses that voted on the proposal along with their vote by doing a range query on `proposalID:addresses` -* `Proposals: int64 => Proposal` maps `proposalID` to the `Proposal` - `proposalID` -* `Options: => VoteType`: maps to the vote of the `voterAddress` for `proposalID` re its delegation to `validatorAddress`. - Returns 0x0 If `voterAddress` has not voted under this validator. -* `ValidatorGovInfos: => ValidatorGovInfo`: maps to the gov info for the `validatorAddress` and `proposalID`. - Returns `nil` if proposal has not entered voting period or if `address` was not the - address of a validator when proposal entered voting period. For pseudocode purposes, here are the two function we will use to read or write in stores: @@ -121,16 +123,14 @@ For pseudocode purposes, here are the two function we will use to read or write `ProposalIDs` of proposals that reached `MinDeposit`. Each round, the oldest element of `ProposalProcessingQueue` is checked during `BeginBlock` to see if `CurrentBlock == VotingStartBlock + InitProcedure.VotingPeriod`. If it is, - then the application checks if validators in `InitVotingPowerList` have voted + then the application tallies the votes, compute the votes of each validator and checks if every validator in the valdiator set have voted and, if not, applies `GovernancePenalty`. If the proposal is accepted, deposits are refunded. After that proposal is ejected from `ProposalProcessingQueue` and the next element of the queue is evaluated. - Note that if a proposal is accepted under the special condition, - its `ProposalID` must be ejected from `ProposalProcessingQueue`. And the pseudocode for the `ProposalProcessingQueue`: ```go - in BeginBlock do + in EndBlock do checkProposal() // First call of the recursive function @@ -141,58 +141,55 @@ And the pseudocode for the `ProposalProcessingQueue`: if (proposalID == nil) return - proposal = load(Proposals, proposalID) + proposal = load(Governance, proposalID) - if (proposal.Votes.YesVotes/proposal.InitTotalVotingPower > 2/3) + if (CurrentBlock == proposal.VotingStartBlock + proposal.Procedure.VotingPeriod && proposal.CurrentStatus == ProposalStatusActive) - // proposal accepted early by super-majority - // no punishments; refund deposits + // End of voting period, tally ProposalProcessingQueue.pop() - var newDeposits []Deposits + validators = stakeKeeper.getAllValidators() + tmpValMap := map(sdk.Address)ValidatorGovInfo - // XXX: why do we need to reset deposits? cant we just clear it ? - for each (amount, depositer) in proposal.Deposits - newDeposits.append[{0, depositer}] - depositer.AtomBalance += amount + // Initiate mapping at 0. Validators that remain at 0 at the end of tally will be punished + for each validator in validators + tmpValMap(validator).Minus = 0 + + voterIterator = rangeQuery(Governance, ) //return all the addresses that voted on the proposal - proposal.Deposits = newDeposits - store(Proposals, proposalID, proposal) + // Tally + for each (voterAddress, vote) in voterIterator + delegations = stakeKeeper.getDelegations(voterAddress) // get all delegations for current voter - checkProposal() + for each delegation in delegations + tmpValMap(delegation.ValidatorAddr).Minus += delegation.Shares + proposal.updateTally(vote, delegation.Shares) - else if (CurrentBlock == proposal.VotingStartBlock + proposal.Procedure.VotingPeriod) + _, isVal = stakeKeeper.getValidator(voterAddress) + if (isVal) + tmpValMap(voterAddress).Vote = vote - ProposalProcessingQueue.pop() - activeProcedure = load(params, 'ActiveProcedure') - - for each validator in CurrentBondedValidators - validatorGovInfo = load(ValidatorGovInfos, ) - - if (validatorGovInfo.InitVotingPower != nil) - // validator was bonded when vote started - - validatorOption = load(Options, ) - if (validatorOption == nil) - // validator did not vote - slash validator by activeProcedure.GovernancePenalty - - - totalNonAbstain = proposal.Votes.YesVotes + proposal.Votes.NoVotes + proposal.Votes.NoWithVetoVotes - if( proposal.Votes.YesVotes/totalNonAbstain > 0.5 AND proposal.Votes.NoWithVetoVotes/totalNonAbstain < 1/3) + // Slash validators that did not vote, or update tally if they voted + for each validator in validators + if (!tmpValMap(validator).HasVoted) + slash validator by proposal.Procedure.GovernancePenalty + else + proposal.updateTally(tmpValMap(validator).Vote, (validator.TotalShares - tmpValMap(validator).Minus)) + // Check if proposal is accepted or rejected + totalNonAbstain := proposal.YesVotes + proposal.NoVotes + proposal.NoWithVetoVotes + if (proposal.Votes.YesVotes/totalNonAbstain > proposal.InitProcedure.Threshold AND proposal.Votes.NoWithVetoVotes/totalNonAbstain < proposal.InitProcedure.Veto) // proposal was accepted at the end of the voting period // refund deposits (non-voters already punished) - - var newDeposits []Deposits - + proposal.CurrentStatus = ProposalStatusAccepted for each (amount, depositer) in proposal.Deposits - newDeposits.append[{0, depositer}] depositer.AtomBalance += amount - proposal.Deposits = newDeposits - store(Proposals, proposalID, proposal) + else + // proposal was rejected + proposal.CurrentStatus = ProposalStatusRejected - checkProposal() + store(Governance, proposalID, proposal) + checkProposal() ``` diff --git a/docs/spec/governance/transactions.md b/docs/spec/governance/transactions.md index 5f5401de87..25beb6d1a2 100644 --- a/docs/spec/governance/transactions.md +++ b/docs/spec/governance/transactions.md @@ -12,7 +12,7 @@ type TxGovSubmitProposal struct { Title string // Title of the proposal Description string // Description of the proposal Type ProposalType // Type of proposal - InitialDeposit int64 // Initial deposit paid by sender. Must be strictly positive. + InitialDeposit sdk.Coins // Initial deposit paid by sender. Must be strictly positive. } ``` @@ -22,8 +22,7 @@ type TxGovSubmitProposal struct { * Initialise `Proposals` attributes * Decrease balance of sender by `InitialDeposit` * If `MinDeposit` is reached: - * Push `proposalID` in `ProposalProcessingQueueEnd` - * Store each validator's voting power in `ValidatorGovInfos` + * Push `proposalID` in `ProposalProcessingQueue` A `TxGovSubmitProposal` transaction can be handled according to the following pseudocode. @@ -34,16 +33,16 @@ pseudocode. upon receiving txGovSubmitProposal from sender do - if !correctlyFormatted(txGovSubmitProposal) then + if !correctlyFormatted(txGovSubmitProposal) // check if proposal is correctly formatted. Includes fee payment. throw initialDeposit = txGovSubmitProposal.InitialDeposit - if (initialDeposit <= 0) OR (sender.AtomBalance < initialDeposit) then + if (initialDeposit.Atoms <= 0) OR (sender.AtomBalance < initialDeposit.Atoms) // InitialDeposit is negative or null OR sender has insufficient funds throw - sender.AtomBalance -= initialDeposit + sender.AtomBalance -= initialDeposit.Atoms proposalID = generate new proposalID proposal = NewProposal() @@ -55,35 +54,25 @@ upon receiving txGovSubmitProposal from sender do proposal.SubmitBlock = CurrentBlock proposal.Deposits.append({initialDeposit, sender}) proposal.Submitter = sender - proposal.Votes.Yes = 0 - proposal.Votes.No = 0 - proposal.Votes.NoWithVeto = 0 - proposal.Votes.Abstain = 0 + proposal.YesVotes = 0 + proposal.NoVotes = 0 + proposal.NoWithVetoVotes = 0 + proposal.AbstainVotes = 0 activeProcedure = load(params, 'ActiveProcedure') - if (initialDeposit < activeProcedure.MinDeposit) then + if (initialDeposit < activeProcedure.MinDeposit) // MinDeposit is not reached + proposal.CurrentStatus = ProposalStatusOpen proposal.VotingStartBlock = -1 - proposal.InitTotalVotingPower = 0 else // MinDeposit is reached + proposal.CurrentStatus = ProposalStatusActive proposal.VotingStartBlock = CurrentBlock - proposal.InitTotalVotingPower = TotalVotingPower proposal.InitProcedure = activeProcedure - - for each validator in CurrentBondedValidators - // Store voting power of each bonded validator - - validatorGovInfo = new ValidatorGovInfo - validatorGovInfo.InitVotingPower = validator.VotingPower - validatorGovInfo.Minus = 0 - - store(ValidatorGovInfos, , validatorGovInfo) - ProposalProcessingQueue.push(proposalID) store(Proposals, proposalID, proposal) // Store proposal in Proposals mapping @@ -98,8 +87,8 @@ Once a proposal is submitted, if ```go type TxGovDeposit struct { - ProposalID int64 // ID of the proposal - Deposit int64 // Number of Atoms to add to the proposal's deposit + ProposalID int64 // ID of the proposal + Deposit sdk.Coins // Number of Atoms to add to the proposal's deposit } ``` @@ -109,7 +98,6 @@ type TxGovDeposit struct { * Increase `proposal.TotalDeposit` by sender's `deposit` * If `MinDeposit` is reached: * Push `proposalID` in `ProposalProcessingQueueEnd` - * Store each validator's voting power in `ValidatorGovInfos` A `TxGovDeposit` transaction has to go through a number of checks to be valid. These checks are outlined in the following pseudocode. @@ -121,57 +109,40 @@ These checks are outlined in the following pseudocode. upon receiving txGovDeposit from sender do // check if proposal is correctly formatted. Includes fee payment. - if !correctlyFormatted(txGovDeposit) then + if !correctlyFormatted(txGovDeposit) throw proposal = load(Proposals, txGovDeposit.ProposalID) - if (proposal == nil) then + if (proposal == nil) // There is no proposal for this proposalID throw - - if (txGovDeposit.Deposit <= 0) OR (sender.AtomBalance < txGovDeposit.Deposit) - // deposit is negative or null OR sender has insufficient funds - throw - + activeProcedure = load(params, 'ActiveProcedure') + + if (txGovDeposit.Deposit.Atoms <= 0) OR (sender.AtomBalance < txGovDeposit.Deposit.Atoms) OR (proposal.TotalDeposit >= activeProcedure.MinDeposit) OR (CurrentBlock >= proposal.SubmitBlock + activeProcedure.MaxDepositPeriod) + // deposit is negative or null + // OR sender has insufficient funds + // OR minDeposit has already been reached + // OR Maximum deposit period reached - if (proposal.TotalDeposit >= activeProcedure.MinDeposit) then - // MinDeposit was reached - // TODO: shouldnt we do something here ? throw - else - if (CurrentBlock >= proposal.SubmitBlock + activeProcedure.MaxDepositPeriod) then - // Maximum deposit period reached - throw + // sender can deposit + sender.AtomBalance -= txGovDeposit.Deposit.Atoms + + proposal.Deposits.append({txGovVote.Deposit, sender}) + proposal.TotalDeposit.Plus(txGovDeposit.Deposit) + + if (proposal.TotalDeposit >= activeProcedure.MinDeposit) + // MinDeposit is reached, vote opens - // sender can deposit - - sender.AtomBalance -= txGovDeposit.Deposit + proposal.VotingStartBlock = CurrentBlock + proposal.CurrentStatus = ProposalStatusActive + proposal.InitProcedure = activeProcedure + ProposalProcessingQueue.push(txGovDeposit.ProposalID) - proposal.Deposits.append({txGovVote.Deposit, sender}) - proposal.TotalDeposit += txGovDeposit.Deposit - - if (proposal.TotalDeposit >= activeProcedure.MinDeposit) then - // MinDeposit is reached, vote opens - - proposal.VotingStartBlock = CurrentBlock - proposal.InitTotalVotingPower = TotalVotingPower - proposal.InitProcedure = activeProcedure - - for each validator in CurrentBondedValidators - // Store voting power of each bonded validator - - validatorGovInfo = NewValidatorGovInfo() - validatorGovInfo.InitVotingPower = validator.VotingPower - validatorGovInfo.Minus = 0 - - store(ValidatorGovInfos, , validatorGovInfo) - - ProposalProcessingQueue.push(txGovDeposit.ProposalID) - - store(Proposals, txGovVote.ProposalID, proposal) + store(Proposals, txGovVote.ProposalID, proposal) ``` ### Vote @@ -183,26 +154,14 @@ vote on the proposal. ```go type TxGovVote struct { ProposalID int64 // proposalID of the proposal - Option string // option from OptionSet chosen by the voter - ValidatorAddress crypto.address // Address of the validator voter wants to tie its vote to + Vote byte // option from OptionSet chosen by the voter } ``` **State modifications:** -* If sender is not a validator and validator has not voted, initialize or - increase minus of validator by sender's `voting power` -* If sender is not a validator and validator has voted, decrease - votes of `validatorOption` by sender's `voting power` -* If sender is not a validator, increase votes of `txGovVote.Option` - by sender's `voting power` -* If sender is a validator, increase votes of `txGovVote.Option` by - validator's `InitVotingPower - minus` (`minus` can be equal to 0) +* Record `Vote` of sender -Votes need to be tied to a validator in order to compute validator's voting -power. If a delegator is bonded to multiple validators, it will have to send -one transaction per validator (the UI should facilitate this so that multiple -transactions can be sent in one "vote flow"). If the sender is the validator -itself, then it will input its own address as `ValidatorAddress` +*Note: Gas cost for this message has to take into account the future tallying of the vote in EndBlocker* Next is a pseudocode proposal of the way `TxGovVote` transactions are handled: @@ -214,91 +173,24 @@ handled: upon receiving txGovVote from sender do // check if proposal is correctly formatted. Includes fee payment. - if !correctlyFormatted(txGovDeposit) then + if !correctlyFormatted(txGovDeposit) throw proposal = load(Proposals, txGovDeposit.ProposalID) - if (proposal == nil) then + if (proposal == nil) // There is no proposal for this proposalID throw - validator = load(CurrentValidators, txGovVote.ValidatorAddress) + + if (proposal.VotingStartBlock >= 0) AND + (CurrentBlock <= proposal.VotingStartBlock + proposal.InitProcedure.VotingPeriod) + + // Sender can vote if + // Vote has started AND if + // Vote had notended + + store(Governance, , txGovVote.Vote) // Voters can vote multiple times. Re-voting overrides previous vote. This is ok because tallying is done once at the end. + - 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 - // ValidatorAddress is not the address of a current validator - - throw - - option = load(Options, ::) - - if (option != nil) - // sender has already voted with the Atoms bonded to ValidatorAddress - throw - - if (proposal.VotingStartBlock < 0) OR - (CurrentBlock > proposal.VotingStartBlock + proposal.InitProcedure.VotingPeriod) OR - (proposal.VotingStartBlock < lastBondingBlock(sender, txGovVote.ValidatorAddress) OR - (proposal.VotingStartBlock < lastUnbondingBlock(sender, txGovVote.Address) OR - (proposal.Votes.YesVotes/proposal.InitTotalVotingPower >= 2/3) then - - // Throws if - // Vote has not started OR if - // Vote had ended OR if - // sender bonded Atoms to ValidatorAddress after start of vote OR if - // sender unbonded Atoms from ValidatorAddress after start of vote OR if - // special condition is met, i.e. proposal is accepted and closed - - throw - - validatorGovInfo = load(ValidatorGovInfos, :) - - if (validatorGovInfo == nil) - // validator became validator after proposal entered voting period - throw - - // sender can vote, check if sender == validator and store sender's option in Options - - store(Options, ::, txGovVote.Option) - - if (sender != validator.address) - // Here, sender is not the Address of the validator whose Address is txGovVote.ValidatorAddress - - if sender does not have bonded Atoms to txGovVote.ValidatorAddress then - // check in Staking module - throw - - validatorOption = load(Options, ::) - - if (validatorOption == nil) - // Validator has not voted already - - validatorGovInfo.Minus += sender.bondedAmounTo(txGovVote.ValidatorAddress) - store(ValidatorGovInfos, :, validatorGovInfo) - - else - // Validator has already voted - // Reduce votes of option chosen by validator by sender's bonded Amount - - proposal.Votes.validatorOption -= sender.bondedAmountTo(txGovVote.ValidatorAddress) - - // increase votes of option chosen by sender by bonded Amount - - senderOption = txGovVote.Option - propoal.Votes.senderOption -= sender.bondedAmountTo(txGovVote.ValidatorAddress) - - store(Proposals, txGovVote.ProposalID, proposal) - - - else - // sender is the address of the validator whose main Address is txGovVote.ValidatorAddress - // i.e. sender == validator - - proposal.Votes.validatorOption += (validatorGovInfo.InitVotingPower - validatorGovInfo.Minus) - - store(Proposals, txGovVote.ProposalID, proposal) ``` From bd8c48106461e295e236e96211bd6c536647a558 Mon Sep 17 00:00:00 2001 From: gamarin Date: Mon, 4 Jun 2018 17:22:38 +0200 Subject: [PATCH 2/8] Small fix --- docs/spec/governance/state.md | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/docs/spec/governance/state.md b/docs/spec/governance/state.md index d82dd92acd..bd68d86a51 100644 --- a/docs/spec/governance/state.md +++ b/docs/spec/governance/state.md @@ -2,13 +2,31 @@ ## State -### Procedures +### Procedures and base types `Procedures` define the rule according to which votes are run. There can only be one active procedure at any given time. If governance wants to change a procedure, either to modify a value or add/remove a parameter, a new procedure has to be created and the previous one rendered inactive. + +```go +type Procedure struct { + VotingPeriod int64 // Length of the voting period. Initial value: 2 weeks + MinDeposit sdk.Coins // Minimum deposit for a proposal to enter voting period. + 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 + GovernancePenalty sdk.Rat // Penalty if validator does not vote + + IsActive bool // If true, procedure is active. Only one procedure can have isActive true. +} +``` + +The current active procedure is stored in a global `params` KVStore. + +And some basic types: + ```go type Vote byte @@ -35,21 +53,8 @@ const ( ProposalStatusAccepted = 0x3 // Proposal has been accepted ProposalStatusRejected = 0x4 // Proposal has been rejected ) - -type Procedure struct { - VotingPeriod int64 // Length of the voting period. Initial value: 2 weeks - MinDeposit sdk.Coins // Minimum deposit for a proposal to enter voting period. - 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 - GovernancePenalty sdk.Rat // Penalty if validator does not vote - - IsActive bool // If true, procedure is active. Only one procedure can have isActive true. -} ``` -The current active procedure is stored in a global `params` KVStore. - ### Deposit ```go From 6f8a2d562c352f84a89b138a7a4a48ff70571e79 Mon Sep 17 00:00:00 2001 From: gamarin Date: Mon, 4 Jun 2018 17:34:31 +0200 Subject: [PATCH 3/8] better display --- docs/spec/governance/state.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/spec/governance/state.md b/docs/spec/governance/state.md index bd68d86a51..eb23a11584 100644 --- a/docs/spec/governance/state.md +++ b/docs/spec/governance/state.md @@ -153,17 +153,17 @@ And the pseudocode for the `ProposalProcessingQueue`: // End of voting period, tally ProposalProcessingQueue.pop() - validators = stakeKeeper.getAllValidators() tmpValMap := map(sdk.Address)ValidatorGovInfo // Initiate mapping at 0. Validators that remain at 0 at the end of tally will be punished for each validator in validators tmpValMap(validator).Minus = 0 - - voterIterator = rangeQuery(Governance, ) //return all the addresses that voted on the proposal + + // Tally + voterIterator = rangeQuery(Governance, ) //return all the addresses that voted on the proposal for each (voterAddress, vote) in voterIterator delegations = stakeKeeper.getDelegations(voterAddress) // get all delegations for current voter @@ -175,6 +175,8 @@ And the pseudocode for the `ProposalProcessingQueue`: if (isVal) tmpValMap(voterAddress).Vote = vote + + // Slash validators that did not vote, or update tally if they voted for each validator in validators if (!tmpValMap(validator).HasVoted) @@ -182,6 +184,8 @@ And the pseudocode for the `ProposalProcessingQueue`: else proposal.updateTally(tmpValMap(validator).Vote, (validator.TotalShares - tmpValMap(validator).Minus)) + + // Check if proposal is accepted or rejected totalNonAbstain := proposal.YesVotes + proposal.NoVotes + proposal.NoWithVetoVotes if (proposal.Votes.YesVotes/totalNonAbstain > proposal.InitProcedure.Threshold AND proposal.Votes.NoWithVetoVotes/totalNonAbstain < proposal.InitProcedure.Veto) From 8c800eb42a0a78d3922c623f42076b73781ff78c Mon Sep 17 00:00:00 2001 From: gamarin Date: Tue, 5 Jun 2018 16:43:56 +0200 Subject: [PATCH 4/8] Sunnys feedback --- docs/spec/governance/state.md | 19 +++++----- docs/spec/governance/transactions.md | 54 ++++++++++++++-------------- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/docs/spec/governance/state.md b/docs/spec/governance/state.md index eb23a11584..0e00bb9718 100644 --- a/docs/spec/governance/state.md +++ b/docs/spec/governance/state.md @@ -52,6 +52,7 @@ const ( ProposalStatusActive = 0x2 // MinDeposit is reachhed, participants can vote ProposalStatusAccepted = 0x3 // Proposal has been accepted ProposalStatusRejected = 0x4 // Proposal has been rejected + ProposalStatusClosed. = 0x5 // Proposal never reached MinDeposit ) ``` @@ -90,7 +91,6 @@ type Proposal struct { Submitter sdk.Address // Address of the submitter VotingStartBlock int64 // Height of the block where MinDeposit was reached. -1 if MinDeposit is not reached - InitProcedure Procedure // Active Procedure when proposal enters voting period CurrentStatus ProposalStatus // Current status of the proposal YesVotes sdk.Rat @@ -112,8 +112,8 @@ We also mention a method to update the tally for a given proposal: We will use one KVStore `Governance` to store two mappings: -* A mapping from `proposalID` to `Proposal` -* A mapping from `proposalID:addresses:address` to `Vote`. This mapping allows us to query all addresses that voted on the proposal along with their vote by doing a range query on `proposalID:addresses` +* A mapping from `proposalID|'proposal'` to `Proposal` +* A mapping from `proposalID|'addresses'|address` to `Vote`. This mapping allows us to query all addresses that voted on the proposal along with their vote by doing a range query on `proposalID:addresses` For pseudocode purposes, here are the two function we will use to read or write in stores: @@ -127,7 +127,7 @@ For pseudocode purposes, here are the two function we will use to read or write * `ProposalProcessingQueue`: A queue `queue[proposalID]` containing all the `ProposalIDs` of proposals that reached `MinDeposit`. Each round, the oldest element of `ProposalProcessingQueue` is checked during `BeginBlock` to see if - `CurrentBlock == VotingStartBlock + InitProcedure.VotingPeriod`. If it is, + `CurrentBlock == VotingStartBlock + activeProcedure.VotingPeriod`. If it is, then the application tallies the votes, compute the votes of each validator and checks if every validator in the valdiator set have voted and, if not, applies `GovernancePenalty`. If the proposal is accepted, deposits are refunded. After that proposal is ejected from `ProposalProcessingQueue` and the next element of the queue is evaluated. @@ -146,9 +146,10 @@ And the pseudocode for the `ProposalProcessingQueue`: if (proposalID == nil) return - proposal = load(Governance, proposalID) + proposal = load(Governance, ) // proposal is a const key + activeProcedure = load(params, 'ActiveProcedure') - if (CurrentBlock == proposal.VotingStartBlock + proposal.Procedure.VotingPeriod && proposal.CurrentStatus == ProposalStatusActive) + if (CurrentBlock == proposal.VotingStartBlock + activeProcedure.VotingPeriod && proposal.CurrentStatus == ProposalStatusActive) // End of voting period, tally @@ -163,7 +164,7 @@ And the pseudocode for the `ProposalProcessingQueue`: // Tally - voterIterator = rangeQuery(Governance, ) //return all the addresses that voted on the proposal + voterIterator = rangeQuery(Governance, ) //return all the addresses that voted on the proposal for each (voterAddress, vote) in voterIterator delegations = stakeKeeper.getDelegations(voterAddress) // get all delegations for current voter @@ -188,7 +189,7 @@ And the pseudocode for the `ProposalProcessingQueue`: // Check if proposal is accepted or rejected totalNonAbstain := proposal.YesVotes + proposal.NoVotes + proposal.NoWithVetoVotes - if (proposal.Votes.YesVotes/totalNonAbstain > proposal.InitProcedure.Threshold AND proposal.Votes.NoWithVetoVotes/totalNonAbstain < proposal.InitProcedure.Veto) + if (proposal.Votes.YesVotes/totalNonAbstain > activeProcedure.Threshold AND proposal.Votes.NoWithVetoVotes/totalNonAbstain < activeProcedure.Veto) // proposal was accepted at the end of the voting period // refund deposits (non-voters already punished) proposal.CurrentStatus = ProposalStatusAccepted @@ -199,6 +200,6 @@ And the pseudocode for the `ProposalProcessingQueue`: // proposal was rejected proposal.CurrentStatus = ProposalStatusRejected - store(Governance, proposalID, proposal) + store(Governance, , proposal) checkProposal() ``` diff --git a/docs/spec/governance/transactions.md b/docs/spec/governance/transactions.md index 25beb6d1a2..f5c39230c7 100644 --- a/docs/spec/governance/transactions.md +++ b/docs/spec/governance/transactions.md @@ -65,17 +65,15 @@ upon receiving txGovSubmitProposal from sender do // MinDeposit is not reached proposal.CurrentStatus = ProposalStatusOpen - proposal.VotingStartBlock = -1 else // MinDeposit is reached proposal.CurrentStatus = ProposalStatusActive proposal.VotingStartBlock = CurrentBlock - proposal.InitProcedure = activeProcedure ProposalProcessingQueue.push(proposalID) - store(Proposals, proposalID, proposal) // Store proposal in Proposals mapping + store(Proposals, , proposal) // Store proposal in Proposals mapping return proposalID ``` @@ -112,7 +110,7 @@ upon receiving txGovDeposit from sender do if !correctlyFormatted(txGovDeposit) throw - proposal = load(Proposals, txGovDeposit.ProposalID) + proposal = load(Proposals, ) // proposal is a const key, proposalID is variable if (proposal == nil) // There is no proposal for this proposalID @@ -120,29 +118,32 @@ upon receiving txGovDeposit from sender do activeProcedure = load(params, 'ActiveProcedure') - if (txGovDeposit.Deposit.Atoms <= 0) OR (sender.AtomBalance < txGovDeposit.Deposit.Atoms) OR (proposal.TotalDeposit >= activeProcedure.MinDeposit) OR (CurrentBlock >= proposal.SubmitBlock + activeProcedure.MaxDepositPeriod) + if (txGovDeposit.Deposit.Atoms <= 0) OR (sender.AtomBalance < txGovDeposit.Deposit.Atoms) OR (proposal.CurrentStatus != ProposalStatusOpen) + // deposit is negative or null // OR sender has insufficient funds - // OR minDeposit has already been reached - // OR Maximum deposit period reached + // OR proposal is not open for deposit anymore throw - - // sender can deposit - sender.AtomBalance -= txGovDeposit.Deposit.Atoms - proposal.Deposits.append({txGovVote.Deposit, sender}) - proposal.TotalDeposit.Plus(txGovDeposit.Deposit) - - if (proposal.TotalDeposit >= activeProcedure.MinDeposit) - // MinDeposit is reached, vote opens + if (CurrentBlock >= proposal.SubmitBlock + activeProcedure.MaxDepositPeriod) + proposal.CurrentStatus = ProposalStatusClosed + + else + // sender can deposit + sender.AtomBalance -= txGovDeposit.Deposit.Atoms + + proposal.Deposits.append({txGovVote.Deposit, sender}) + proposal.TotalDeposit.Plus(txGovDeposit.Deposit) - proposal.VotingStartBlock = CurrentBlock - proposal.CurrentStatus = ProposalStatusActive - proposal.InitProcedure = activeProcedure - ProposalProcessingQueue.push(txGovDeposit.ProposalID) + if (proposal.TotalDeposit >= activeProcedure.MinDeposit) + // MinDeposit is reached, vote opens + + proposal.VotingStartBlock = CurrentBlock + proposal.CurrentStatus = ProposalStatusActive + ProposalProcessingQueue.push(txGovDeposit.ProposalID) - store(Proposals, txGovVote.ProposalID, proposal) + store(Proposals, , proposal) ``` ### Vote @@ -153,7 +154,7 @@ vote on the proposal. ```go type TxGovVote struct { - ProposalID int64 // proposalID of the proposal + ProposalID int64 // proposalID of the proposal Vote byte // option from OptionSet chosen by the voter } ``` @@ -176,21 +177,18 @@ handled: if !correctlyFormatted(txGovDeposit) throw - proposal = load(Proposals, txGovDeposit.ProposalID) + proposal = load(Proposals, ) if (proposal == nil) // There is no proposal for this proposalID throw - if (proposal.VotingStartBlock >= 0) AND - (CurrentBlock <= proposal.VotingStartBlock + proposal.InitProcedure.VotingPeriod) + if (proposal.CurrentStatus == ProposalStatusActive) - // Sender can vote if - // Vote has started AND if - // Vote had notended + // Sender can vote - store(Governance, , txGovVote.Vote) // Voters can vote multiple times. Re-voting overrides previous vote. This is ok because tallying is done once at the end. + store(Governance, , txGovVote.Vote) // Voters can vote multiple times. Re-voting overrides previous vote. This is ok because tallying is done once at the end. ``` From b52598931912e53dff5bba03b374723f366f61bb Mon Sep 17 00:00:00 2001 From: gamarin Date: Thu, 7 Jun 2018 12:02:21 +0200 Subject: [PATCH 5/8] Split procedures and add grace period --- docs/spec/governance/state.md | 38 +++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/docs/spec/governance/state.md b/docs/spec/governance/state.md index 0e00bb9718..876c457eb9 100644 --- a/docs/spec/governance/state.md +++ b/docs/spec/governance/state.md @@ -11,21 +11,30 @@ has to be created and the previous one rendered inactive. ```go -type Procedure struct { - VotingPeriod int64 // Length of the voting period. Initial value: 2 weeks +type DepositProcedure struct { MinDeposit sdk.Coins // Minimum deposit for a proposal to enter voting period. - 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 - GovernancePenalty sdk.Rat // Penalty if validator does not vote - - IsActive bool // If true, procedure is active. Only one procedure can have isActive true. } ``` -The current active procedure is stored in a global `params` KVStore. +```go +type VotingProcedure struct { + VotingPeriod int64 // Length of the voting period. Initial value: 2 weeks +} +``` -And some basic types: +```go +type TallyingProcedure struct { + Threshold rational.Rational // Minimum propotion of Yes votes for proposal to pass. Initial value: 0.5 + Veto rational.Rational // Minimum proportion of Veto votes to Total votes ratio for proposal to be vetoed. Initial value: 1/3 + GovernancePenalty sdk.Rat // Penalty if validator does not vote + GracePeriod int64 // If validator entered validator set in this period of blocks before vote ended, governance penalty does not apply +} +``` + +Procedures are stored in a global `GlobalParams` KVStore. + +Additionally, we introduce some basic types: ```go @@ -169,6 +178,7 @@ And the pseudocode for the `ProposalProcessingQueue`: delegations = stakeKeeper.getDelegations(voterAddress) // get all delegations for current voter for each delegation in delegations + // make sure delegation.Shares does NOT include shares being unbonded tmpValMap(delegation.ValidatorAddr).Minus += delegation.Shares proposal.updateTally(vote, delegation.Shares) @@ -180,10 +190,12 @@ And the pseudocode for the `ProposalProcessingQueue`: // Slash validators that did not vote, or update tally if they voted for each validator in validators - if (!tmpValMap(validator).HasVoted) - slash validator by proposal.Procedure.GovernancePenalty - else - proposal.updateTally(tmpValMap(validator).Vote, (validator.TotalShares - tmpValMap(validator).Minus)) + if (validator.bondHeight < CurrentBlock - activeProcedure.GracePeriod) + // only slash if validator entered validator set before grace period + if (!tmpValMap(validator).HasVoted) + slash validator by activeProcedure.GovernancePenalty + else + proposal.updateTally(tmpValMap(validator).Vote, (validator.TotalShares - tmpValMap(validator).Minus)) From 09ea8dac11406a23a91ab15464a383688ae46148 Mon Sep 17 00:00:00 2001 From: gamarin Date: Thu, 7 Jun 2018 12:30:49 +0200 Subject: [PATCH 6/8] additional chekc in VoteMsg --- docs/spec/governance/transactions.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/spec/governance/transactions.md b/docs/spec/governance/transactions.md index f5c39230c7..6192f908de 100644 --- a/docs/spec/governance/transactions.md +++ b/docs/spec/governance/transactions.md @@ -184,9 +184,11 @@ handled: throw - if (proposal.CurrentStatus == ProposalStatusActive) + if (proposal.CurrentStatus == ProposalStatusActive && len(stakeKeeper.GetDelegations(sender)) > 0) - // Sender can vote + // Sender can vote if + // Proposal is active + // Sender has some bonds store(Governance, , txGovVote.Vote) // Voters can vote multiple times. Re-voting overrides previous vote. This is ok because tallying is done once at the end. From 9d5425b806e93102d67ac9e6bc8638b80a98fec1 Mon Sep 17 00:00:00 2001 From: gamarin Date: Fri, 15 Jun 2018 16:18:50 +0200 Subject: [PATCH 7/8] latest change --- docs/spec/governance/state.md | 24 ++++++++++++++---------- docs/spec/governance/transactions.md | 14 ++++++++------ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/docs/spec/governance/state.md b/docs/spec/governance/state.md index 876c457eb9..15df741cdb 100644 --- a/docs/spec/governance/state.md +++ b/docs/spec/governance/state.md @@ -50,8 +50,9 @@ const ( type ProposalType byte const ( - ProposalTypePlainText = 0x1 - ProposalTypeSoftwareUpgrade = 0x2 + ProposalTypePlainText = 0x1 // Plain text proposals + ProposalTypeSoftwareUpgrade = 0x2 // Text proposal inducing a software upgrade + ProposalTypeParameterChange = 0x3 // Add or change a parameter in GlobalParams store ) type ProposalStatus byte @@ -156,14 +157,17 @@ And the pseudocode for the `ProposalProcessingQueue`: return proposal = load(Governance, ) // proposal is a const key - activeProcedure = load(params, 'ActiveProcedure') + votingProcedure = load(GlobalParams, 'VotingProcedure') - if (CurrentBlock == proposal.VotingStartBlock + activeProcedure.VotingPeriod && proposal.CurrentStatus == ProposalStatusActive) + if (CurrentBlock == proposal.VotingStartBlock + votingProcedure.VotingPeriod && proposal.CurrentStatus == ProposalStatusActive) // End of voting period, tally ProposalProcessingQueue.pop() - validators = stakeKeeper.getAllValidators() + validators = + + + Keeper.getAllValidators() tmpValMap := map(sdk.Address)ValidatorGovInfo // Initiate mapping at 0. Validators that remain at 0 at the end of tally will be punished @@ -184,16 +188,16 @@ And the pseudocode for the `ProposalProcessingQueue`: _, isVal = stakeKeeper.getValidator(voterAddress) if (isVal) - tmpValMap(voterAddress).Vote = vote - + tmpValMap(voterAddress).Vote = vote + tallyingProcedure = load(GlobalParams, 'TallyingProcedure') // Slash validators that did not vote, or update tally if they voted for each validator in validators - if (validator.bondHeight < CurrentBlock - activeProcedure.GracePeriod) + if (validator.bondHeight < CurrentBlock - tallyingProcedure.GracePeriod) // only slash if validator entered validator set before grace period if (!tmpValMap(validator).HasVoted) - slash validator by activeProcedure.GovernancePenalty + slash validator by tallyingProcedure.GovernancePenalty else proposal.updateTally(tmpValMap(validator).Vote, (validator.TotalShares - tmpValMap(validator).Minus)) @@ -201,7 +205,7 @@ And the pseudocode for the `ProposalProcessingQueue`: // Check if proposal is accepted or rejected totalNonAbstain := proposal.YesVotes + proposal.NoVotes + proposal.NoWithVetoVotes - if (proposal.Votes.YesVotes/totalNonAbstain > activeProcedure.Threshold AND proposal.Votes.NoWithVetoVotes/totalNonAbstain < activeProcedure.Veto) + if (proposal.Votes.YesVotes/totalNonAbstain > tallyingProcedure.Threshold AND proposal.Votes.NoWithVetoVotes/totalNonAbstain < tallyingProcedure.Veto) // proposal was accepted at the end of the voting period // refund deposits (non-voters already punished) proposal.CurrentStatus = ProposalStatusAccepted diff --git a/docs/spec/governance/transactions.md b/docs/spec/governance/transactions.md index 6192f908de..60089f32ed 100644 --- a/docs/spec/governance/transactions.md +++ b/docs/spec/governance/transactions.md @@ -41,6 +41,8 @@ upon receiving txGovSubmitProposal from sender do if (initialDeposit.Atoms <= 0) OR (sender.AtomBalance < initialDeposit.Atoms) // InitialDeposit is negative or null OR sender has insufficient funds throw + + if (txGovSubmitProposal.Type != ProposalTypePlainText) OR (txGovSubmitProposal.Type != ProposalTypeSoftwareUpgrade) sender.AtomBalance -= initialDeposit.Atoms @@ -59,9 +61,9 @@ upon receiving txGovSubmitProposal from sender do proposal.NoWithVetoVotes = 0 proposal.AbstainVotes = 0 - activeProcedure = load(params, 'ActiveProcedure') + depositProcedure = load(GlobalParams, 'DepositProcedure') - if (initialDeposit < activeProcedure.MinDeposit) + if (initialDeposit < depositProcedure.MinDeposit) // MinDeposit is not reached proposal.CurrentStatus = ProposalStatusOpen @@ -115,8 +117,6 @@ upon receiving txGovDeposit from sender do if (proposal == nil) // There is no proposal for this proposalID throw - - activeProcedure = load(params, 'ActiveProcedure') if (txGovDeposit.Deposit.Atoms <= 0) OR (sender.AtomBalance < txGovDeposit.Deposit.Atoms) OR (proposal.CurrentStatus != ProposalStatusOpen) @@ -126,7 +126,9 @@ upon receiving txGovDeposit from sender do throw - if (CurrentBlock >= proposal.SubmitBlock + activeProcedure.MaxDepositPeriod) + depositProcedure = load(GlobalParams, 'DepositProcedure') + + if (CurrentBlock >= proposal.SubmitBlock + depositProcedure.MaxDepositPeriod) proposal.CurrentStatus = ProposalStatusClosed else @@ -136,7 +138,7 @@ upon receiving txGovDeposit from sender do proposal.Deposits.append({txGovVote.Deposit, sender}) proposal.TotalDeposit.Plus(txGovDeposit.Deposit) - if (proposal.TotalDeposit >= activeProcedure.MinDeposit) + if (proposal.TotalDeposit >= depositProcedure.MinDeposit) // MinDeposit is reached, vote opens proposal.VotingStartBlock = CurrentBlock From b2e9e1724bc7eba33c74bef69ee5f75e70727fc7 Mon Sep 17 00:00:00 2001 From: gamarin Date: Mon, 2 Jul 2018 13:50:55 +0200 Subject: [PATCH 8/8] Spec conforms to current module --- docs/spec/governance/state.md | 1 - docs/spec/governance/transactions.md | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/spec/governance/state.md b/docs/spec/governance/state.md index 15df741cdb..fa1ca7d6a9 100644 --- a/docs/spec/governance/state.md +++ b/docs/spec/governance/state.md @@ -52,7 +52,6 @@ type ProposalType byte const ( ProposalTypePlainText = 0x1 // Plain text proposals ProposalTypeSoftwareUpgrade = 0x2 // Text proposal inducing a software upgrade - ProposalTypeParameterChange = 0x3 // Add or change a parameter in GlobalParams store ) type ProposalStatus byte diff --git a/docs/spec/governance/transactions.md b/docs/spec/governance/transactions.md index 60089f32ed..32b8893d82 100644 --- a/docs/spec/governance/transactions.md +++ b/docs/spec/governance/transactions.md @@ -186,7 +186,7 @@ handled: throw - if (proposal.CurrentStatus == ProposalStatusActive && len(stakeKeeper.GetDelegations(sender)) > 0) + if (proposal.CurrentStatus == ProposalStatusActive) // Sender can vote if // Proposal is active