Merge pull request #1129 from cosmos/gamarin/update_gov_spec

WIP: Update gov spec
This commit is contained in:
Ethan Buchman 2018-07-13 20:00:17 +01:00 committed by GitHub
commit 32496c7b89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 166 additions and 246 deletions

View File

@ -2,46 +2,69 @@
## 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 DepositProcedure struct {
MinDeposit sdk.Coins // Minimum deposit for a proposal to enter voting period.
MaxDepositPeriod int64 // Maximum period for Atom holders to deposit on a proposal. Initial value: 2 months
}
```
```go
type VotingProcedure struct {
VotingPeriod int64 // Length of the voting period. Initial value: 2 weeks
}
```
```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
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
ProposalTypeSoftwareUpgrade = 0x2
ProposalTypePlainText = 0x1 // Plain text proposals
ProposalTypeSoftwareUpgrade = 0x2 // Text proposal inducing a software upgrade
)
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.
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
GovernancePenalty int64 // Penalty if validator does not vote
IsActive bool // If true, procedure is active. Only one procedure can have isActive true.
}
```
type ProposalStatus byte
The current active procedure is stored in a global `params` KVStore.
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
ProposalStatusClosed. = 0x5 // Proposal never reached MinDeposit
)
```
### Deposit
@ -52,18 +75,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.
@ -76,37 +98,33 @@ 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|'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`
* `Proposals: int64 => Proposal` maps `proposalID` to the `Proposal`
`proposalID`
* `Options: <proposalID | voterAddress | Address> => VoteType`: maps to the vote of the `voterAddress` for `proposalID` re its delegation to `Address`.
Returns 0x0 If `voterAddress` has not voted under this validator.
* `ValidatorGovInfos: <proposalID | Address> => ValidatorGovInfo`: maps to the gov info for the `Address` 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:
@ -119,17 +137,15 @@ 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,
then the application checks if validators in `InitVotingPowerList` have voted
`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.
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
@ -140,58 +156,66 @@ And the pseudocode for the `ProposalProcessingQueue`:
if (proposalID == nil)
return
proposal = load(Proposals, proposalID)
proposal = load(Governance, <proposalID|'proposal'>) // proposal is a const key
votingProcedure = load(GlobalParams, 'VotingProcedure')
if (proposal.Votes.YesVotes/proposal.InitTotalVotingPower > 2/3)
if (CurrentBlock == proposal.VotingStartBlock + votingProcedure.VotingPeriod && proposal.CurrentStatus == ProposalStatusActive)
// proposal accepted early by super-majority
// no punishments; refund deposits
// End of voting period, tally
ProposalProcessingQueue.pop()
var newDeposits []Deposits
// 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
proposal.Deposits = newDeposits
store(Proposals, proposalID, proposal)
checkProposal()
else if (CurrentBlock == proposal.VotingStartBlock + proposal.Procedure.VotingPeriod)
ProposalProcessingQueue.pop()
activeProcedure = load(params, 'ActiveProcedure')
for each validator in CurrentBondedValidators
validatorGovInfo = load(ValidatorGovInfos, <proposalID | validator.Address>)
if (validatorGovInfo.InitVotingPower != nil)
// validator was bonded when vote started
validatorOption = load(Options, <proposalID | validator.Address>)
if (validatorOption == nil)
// validator did not vote
slash validator by activeProcedure.GovernancePenalty
validators =
totalNonAbstain = proposal.Votes.YesVotes + proposal.Votes.NoVotes + proposal.Votes.NoWithVetoVotes
if( proposal.Votes.YesVotes/totalNonAbstain > 0.5 AND proposal.Votes.NoWithVetoVotes/totalNonAbstain < 1/3)
Keeper.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
// Tally
voterIterator = rangeQuery(Governance, <proposalID|'addresses'>) //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
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)
_, isVal = stakeKeeper.getValidator(voterAddress)
if (isVal)
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 - tallyingProcedure.GracePeriod)
// only slash if validator entered validator set before grace period
if (!tmpValMap(validator).HasVoted)
slash validator by tallyingProcedure.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 > 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)
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'>, proposal)
checkProposal()
```

View File

@ -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,18 @@ 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
if (txGovSubmitProposal.Type != ProposalTypePlainText) OR (txGovSubmitProposal.Type != ProposalTypeSoftwareUpgrade)
sender.AtomBalance -= initialDeposit
sender.AtomBalance -= initialDeposit.Atoms
proposalID = generate new proposalID
proposal = NewProposal()
@ -55,38 +56,26 @@ 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')
depositProcedure = load(GlobalParams, 'DepositProcedure')
if (initialDeposit < activeProcedure.MinDeposit) then
if (initialDeposit < depositProcedure.MinDeposit)
// MinDeposit is not reached
proposal.VotingStartBlock = -1
proposal.InitTotalVotingPower = 0
proposal.CurrentStatus = ProposalStatusOpen
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, <proposalID | validator.Address>, validatorGovInfo)
ProposalProcessingQueue.push(proposalID)
store(Proposals, proposalID, proposal) // Store proposal in Proposals mapping
store(Proposals, <proposalID|'proposal'>, proposal) // Store proposal in Proposals mapping
return proposalID
```
@ -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,43 @@ 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)
proposal = load(Proposals, <txGovDeposit.ProposalID|'proposal'>) // proposal is a const key, proposalID is variable
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.CurrentStatus != ProposalStatusOpen)
// deposit is negative or null
// OR sender has insufficient funds
// OR proposal is not open for deposit anymore
if (proposal.TotalDeposit >= activeProcedure.MinDeposit) then
// MinDeposit was reached
// TODO: shouldnt we do something here ?
throw
depositProcedure = load(GlobalParams, 'DepositProcedure')
if (CurrentBlock >= proposal.SubmitBlock + depositProcedure.MaxDepositPeriod)
proposal.CurrentStatus = ProposalStatusClosed
else
if (CurrentBlock >= proposal.SubmitBlock + activeProcedure.MaxDepositPeriod) then
// Maximum deposit period reached
throw
// sender can deposit
sender.AtomBalance -= txGovDeposit.Deposit
sender.AtomBalance -= txGovDeposit.Deposit.Atoms
proposal.Deposits.append({txGovVote.Deposit, sender})
proposal.TotalDeposit += txGovDeposit.Deposit
proposal.TotalDeposit.Plus(txGovDeposit.Deposit)
if (proposal.TotalDeposit >= activeProcedure.MinDeposit) then
if (proposal.TotalDeposit >= depositProcedure.MinDeposit)
// 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, <proposalID | validator.Address>, validatorGovInfo)
proposal.CurrentStatus = ProposalStatusActive
ProposalProcessingQueue.push(txGovDeposit.ProposalID)
store(Proposals, txGovVote.ProposalID, proposal)
store(Proposals, <txGovVote.ProposalID|'proposal'>, proposal)
```
### Vote
@ -182,27 +156,16 @@ vote on the proposal.
```go
type TxGovVote struct {
ProposalID int64 // proposalID of the proposal
Option string // option chosen by the voter
ValidatorAddress crypto.address // Address of the validator voter wants to tie its vote to
ProposalID int64 // proposalID of the proposal
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
*Note: Gas cost for this message has to take into account the future tallying of the vote in EndBlocker*
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 `Address`
Next is a pseudocode proposal of the way `TxGovVote` transactions are
handled:
@ -214,89 +177,22 @@ 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)
proposal = load(Proposals, <txGovDeposit.ProposalID|'proposal'>)
if (proposal == nil) then
if (proposal == nil)
// There is no proposal for this proposalID
throw
validator = load(CurrentValidators, txGovVote.Address)
if (validator == nil) then
// Throws if
// ValidatorAddress is not the address of a current validator
throw
else
option = load(Options, <txGovVote.ProposalID>:<sender>:<txGovVote.ValidatorAddress>)
if (option != nil)
// sender has already voted with the Atoms bonded to Address
throw
if (proposal.CurrentStatus == ProposalStatusActive)
if (proposal.VotingStartBlock < 0) OR
(CurrentBlock > proposal.VotingStartBlock + proposal.InitProcedure.VotingPeriod) OR
(proposal.VotingStartBlock < lastBondingBlock(sender, txGovVote.Address) 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 Address after start of vote OR if
// sender unbonded Atoms from Address after start of vote OR if
// special condition is met, i.e. proposal is accepted and closed
// Sender can vote if
// Proposal is active
// Sender has some bonds
throw
validatorGovInfo = load(ValidatorGovInfos, <txGovVote.ProposalID>:<validator.Address>)
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.ProposalID>:<sender>:<txGovVote.Address>, txGovVote.Option)
if (sender != validator.address)
// Here, sender is not the Address of the validator whose Address is txGovVote.Address
if sender does not have bonded Atoms to txGovVote.Address then
// check in Staking module
throw
validatorOption = load(Options, <txGovVote.ProposalID>:<sender>:<txGovVote.Address>)
if (validatorOption == nil)
// Validator has not voted already
validatorGovInfo.Minus += sender.bondedAmounTo(txGovVote.Address)
store(ValidatorGovInfos, <txGovVote.ProposalID>:<validator.Address>, validatorGovInfo)
else
// Validator has already voted
// Reduce votes of option chosen by validator by sender's bonded Amount
proposal.Votes.validatorOption -= sender.bondedAmountTo(txGovVote.Address)
// increase votes of option chosen by sender by bonded Amount
senderOption = txGovVote.Option
propoal.Votes.senderOption -= sender.bondedAmountTo(txGovVote.Address)
store(Proposals, txGovVote.ProposalID, proposal)
else
// sender is the address of the validator whose main Address is txGovVote.Address
// i.e. sender == validator
proposal.Votes.validatorOption += (validatorGovInfo.InitVotingPower - validatorGovInfo.Minus)
store(Proposals, txGovVote.ProposalID, proposal)
store(Governance, <txGovVote.ProposalID|'addresses'|sender>, txGovVote.Vote) // Voters can vote multiple times. Re-voting overrides previous vote. This is ok because tallying is done once at the end.
```