Merge branch 'develop' of https://github.com/cosmos/cosmos-sdk into develop
This commit is contained in:
commit
d3461d8088
|
@ -28,6 +28,10 @@ FEATURES:
|
|||
* New genesis account keys are automatically added to the client keybase (introduce `--client-home` flag)
|
||||
* Initialize with genesis txs using `--gen-txs` flag
|
||||
* Context now has access to the application-configured logger
|
||||
* Add (non-proof) subspace query helper functions
|
||||
* Add more staking query functions: candidates, delegator-bonds
|
||||
* Bank module now tags transactions with sender/recipient for indexing & later retrieval
|
||||
* Stake module now tags transactions with delegator/candidate for delegation & unbonding, and candidate info for declare candidate / edit candidacy
|
||||
|
||||
BUG FIXES
|
||||
* Gaia now uses stake, ported from github.com/cosmos/gaia
|
||||
|
|
|
@ -26,7 +26,7 @@ ADD . $REPO_PATH
|
|||
|
||||
# Install minimum necessary dependencies, build Cosmos SDK, remove packages
|
||||
RUN apk add --no-cache $PACKAGES && \
|
||||
cd $REPO_PATH && make get_tools && make get_vendor_deps && make all && make install && \
|
||||
cd $REPO_PATH && make get_tools && make get_vendor_deps && make build && make install && \
|
||||
apk del $PACKAGES
|
||||
|
||||
# Set entrypoint
|
||||
|
|
|
@ -43,8 +43,23 @@ func (ctx CoreContext) BroadcastTx(tx []byte) (*ctypes.ResultBroadcastTxCommit,
|
|||
|
||||
// Query from Tendermint with the provided key and storename
|
||||
func (ctx CoreContext) Query(key cmn.HexBytes, storeName string) (res []byte, err error) {
|
||||
return ctx.query(key, storeName, "key")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/%s/key", storeName)
|
||||
// Query from Tendermint with the provided storename and subspace
|
||||
func (ctx CoreContext) QuerySubspace(cdc *wire.Codec, subspace []byte, storeName string) (res []sdk.KVPair, err error) {
|
||||
resRaw, err := ctx.query(subspace, storeName, "subspace")
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
cdc.MustUnmarshalBinary(resRaw, &res)
|
||||
return
|
||||
}
|
||||
|
||||
// Query from Tendermint with the provided storename and path
|
||||
func (ctx CoreContext) query(key cmn.HexBytes, storeName, endPath string) (res []byte, err error) {
|
||||
|
||||
path := fmt.Sprintf("/%s/%s", storeName, endPath)
|
||||
node, err := ctx.GetNode()
|
||||
if err != nil {
|
||||
return res, err
|
||||
|
|
|
@ -46,7 +46,7 @@ func main() {
|
|||
client.GetCommands(
|
||||
authcmd.GetAccountCmd("acc", cdc, authcmd.GetAccountDecoder(cdc)),
|
||||
stakecmd.GetCmdQueryCandidate("stake", cdc),
|
||||
//stakecmd.GetCmdQueryCandidates("stake", cdc),
|
||||
stakecmd.GetCmdQueryCandidates("stake", cdc),
|
||||
stakecmd.GetCmdQueryDelegatorBond("stake", cdc),
|
||||
//stakecmd.GetCmdQueryDelegatorBonds("stake", cdc),
|
||||
)...)
|
||||
|
|
|
@ -1,659 +0,0 @@
|
|||
# Governance documentation
|
||||
|
||||
*Disclaimer: This is work in progress. Mechanisms are susceptible to change.*
|
||||
|
||||
This document describes the high-level architecture of the governance module. The governance module allows bonded Atom holders to vote on proposals on a 1 bonded Atom 1 vote basis.
|
||||
|
||||
## Design overview
|
||||
|
||||
The governance process is divided in a few steps that are outlined below:
|
||||
|
||||
- **Proposal submission:** Proposal is submitted to the blockchain with a deposit
|
||||
- **Vote:** Once deposit reaches a certain value (`MinDeposit`), proposal is confirmed and vote opens. Bonded Atom holders can then send `TxGovVote` transactions to vote on the proposal
|
||||
- If the proposal involves a software upgrade:
|
||||
- **Signal:** Validators start signaling that they are ready to switch to the new version
|
||||
- **Switch:** Once more than 75% of validators have signaled that they are ready to switch, their software automatically flips to the new version
|
||||
|
||||
## Proposal submission
|
||||
|
||||
### Right to submit a proposal
|
||||
|
||||
Any Atom holder, whether bonded or unbonded, can submit proposals by sending a `TxGovProposal` transaction. Once a proposal is submitted, it is identified by its unique `proposalID`.
|
||||
|
||||
### Proposal filter (minimum deposit)
|
||||
|
||||
To prevent spam, proposals must be submitted with a deposit in Atoms. Voting period will not start as long as the proposal's deposit is smaller than the minimum deposit `MinDeposit`.
|
||||
|
||||
When a proposal is submitted, it has to be accompagnied by a deposit that must be strictly positive but can be inferior to `MinDeposit`. Indeed, the submitter need not pay for the entire deposit on its own. If a proposal's deposit is strictly inferior to `MinDeposit`, other Atom holders can increase the proposal's deposit by sending a `TxGovDeposit` transaction. Once the proposals's deposit reaches `MinDeposit`, it enters voting period.
|
||||
|
||||
### Deposit refund
|
||||
|
||||
There are two instances where Atom holders that deposited can claim back their deposit:
|
||||
- If the proposal is accepted
|
||||
- If the proposal's deposit does not reach `MinDeposit` for a period longer than `MaxDepositPeriod` (initial value: 2 months). Then the proposal is considered closed and nobody can deposit on it anymore.
|
||||
|
||||
In such instances, Atom holders that deposited can send a `TxGovClaimDeposit` transaction to retrieve their share of the deposit.
|
||||
|
||||
### Proposal types
|
||||
|
||||
In the initial version of the governance module, there are two types of proposal:
|
||||
- `PlainTextProposal`. All the proposals that do not involve a modification of the source code go under this type. For example, an opinion poll would use a proposal of type `PlainTextProposal`
|
||||
- `SoftwareUpgradeProposal`. If accepted, validators are expected to update their software in accordance with the proposal. They must do so by following a 2-steps process described in the [Software Upgrade](#software-upgrade) section below. Software upgrade roadmap may be discussed and agreed on via `PlainTextProposals`, but actual software upgrades must be performed via `SoftwareUpgradeProposals`.
|
||||
|
||||
### Proposal categories
|
||||
|
||||
There are two categories of proposal:
|
||||
- `Regular`
|
||||
- `Urgent`
|
||||
|
||||
These two categories are strictly identical except that `Urgent` proposals can be accepted faster if a certain condition is met. For more information, see [Threshold](#threshold) section.
|
||||
|
||||
## Vote
|
||||
|
||||
### Participants
|
||||
|
||||
*Participants* are users that have the right to vote on proposals. On the Cosmos Hub, participants are bonded Atom holders. Unbonded Atom holders and other users do not get the right to participate in governance. However, they can submit and deposit on proposals.
|
||||
|
||||
Note that some *participants* can be forbidden to vote on a proposal under a certain validator if:
|
||||
- *participant* bonded or unbonded Atoms to said validator after proposal entered voting period
|
||||
- *participant* became validator after proposal entered voting period
|
||||
|
||||
This does not prevent *participant* to vote with Atoms bonded to other validators. For example, if a *participant* bonded some Atoms to validator A before a proposal entered voting period and other Atoms to validator B after proposal entered voting period, only the vote under validator B will be forbidden.
|
||||
|
||||
### Voting period
|
||||
|
||||
Once a proposal reaches `MinDeposit`, it immediately enters `Voting period`. We define `Voting period` as the interval between the moment the vote opens and the moment the vote closes. `Voting period` should always be shorter than `Unbonding period` to prevent double voting. The initial value of `Voting period` is 2 weeks.
|
||||
|
||||
### Option set
|
||||
|
||||
The option set of a proposal refers to the set of choices a participant can choose from when casting its vote.
|
||||
|
||||
The initial option set includes the following options:
|
||||
- `Yes`
|
||||
- `No`
|
||||
- `NoWithVeto`
|
||||
- `Abstain`
|
||||
|
||||
`NoWithVeto` counts as `No` but also adds a `Veto` vote. `Abstain` option allows voters to signal that they do not intend to vote in favor or against the proposal but accept the result of the vote.
|
||||
|
||||
*Note: from the UI, for urgent proposals we should maybe add a ‘Not Urgent’ option that casts a `NoWithVeto` vote.*
|
||||
|
||||
### Quorum
|
||||
|
||||
Quorum is defined as the minimum percentage of voting power that needs to be casted on a proposal for the result to be valid.
|
||||
|
||||
In the initial version of the governance module, there will be no quorum enforced by the protocol. Participation is ensured via the combination of inheritance and validator's punishment for non-voting.
|
||||
|
||||
### Threshold
|
||||
|
||||
Threshold is defined as the minimum proportion of `Yes` votes (excluding `Abstain` votes) for the proposal to be accepted.
|
||||
|
||||
Initially, the threshold is set at 50% with a possibility to veto if more than 1/3rd of votes (excluding `Abstain` votes) are `NoWithVeto` votes. This means that proposals are accepted if the proportion of `Yes` votes (excluding `Abstain` votes) at the end of the voting period is superior to 50% and if the proportion of `NoWithVeto` votes is inferior to 1/3 (excluding `Abstain` votes).
|
||||
|
||||
`Urgent` proposals also work with the aforementioned threshold, except there is another condition that can accelerate the acceptance of the proposal. Namely, if the ratio of `Yes` votes to `InitTotalVotingPower` exceeds 2:3, `UrgentProposal` will be immediately accepted, even if the `Voting period` is not finished. `InitTotalVotingPower` is the total voting power of all bonded Atom holders at the moment when the vote opens.
|
||||
|
||||
### Inheritance
|
||||
|
||||
If a delegator does not vote, it will inherit its validator vote.
|
||||
|
||||
- If the delegator votes before its validator, it will not inherit from the validator's vote.
|
||||
- If the delegator votes after its validator, it will override its validator vote with its own. If the proposal is a `Urgent` proposal, it is possible that the vote will close before delegators have a chance to react and override their validator's vote. This is not a problem, as `Urgent` proposals require more than 2/3rd of the total voting power to pass before the end of the voting period. If more than 2/3rd of validators collude, they can censor the votes of delegators anyway.
|
||||
|
||||
### Validator’s punishment for non-voting
|
||||
|
||||
Validators are required to vote on all proposals to ensure that results have legitimacy. Voting is part of validators' directives and failure to do it will result in a penalty.
|
||||
|
||||
If a validator’s address is not in the list of addresses that voted on a proposal and the vote is closed (i.e. `MinDeposit` was reached and `Voting period` is over), then the validator will automatically be partially slashed of `GovernancePenalty`.
|
||||
|
||||
*Note: Need to define values for `GovernancePenalty`*
|
||||
|
||||
**Exception:** If a proposal is a `Urgent` proposal and is accepted via the special condition of having a ratio of `Yes` votes to `InitTotalVotingPower` that exceeds 2:3, validators cannot be punished for not having voted on it. That is because the proposal will close as soon as the ratio exceeds 2:3, making it mechanically impossible for some validators to vote on it.
|
||||
|
||||
### Governance key and governance address
|
||||
|
||||
Validators can make use of a slot where they can designate a `Governance PubKey`. By default, a validator's `Governance PubKey` will be the same as its main PubKey. Validators can change this `Governance PubKey` by sending a `Change Governance PubKey` transaction signed by their main `Consensus PrivKey`. From there, they will be able to sign votes using the `Governance PrivKey` associated with their `Governance PubKey`. The `Governance PubKey` can be changed at any moment.
|
||||
|
||||
|
||||
## Software Upgrade
|
||||
|
||||
If proposals are of type `SoftwareUpgradeProposal`, then nodes need to upgrade their software to the new version that was voted. This process is divided in two steps.
|
||||
|
||||
### Signal
|
||||
|
||||
After a `SoftwareUpgradeProposal` is accepted, validators are expected to download and install the new version of the software while continuing to run the previous version. Once a validator has downloaded and installed the upgrade, it will start signaling to the network that it is ready to switch by including the proposal's `proposalID` in its *precommits*.(*Note: Confirmation that we want it in the precommit?*)
|
||||
|
||||
Note: There is only one signal slot per *precommit*. If several `SoftwareUpgradeProposals` are accepted in a short timeframe, a pipeline will form and they will be implemented one after the other in the order that they were accepted.
|
||||
|
||||
### Switch
|
||||
|
||||
Once a block contains more than 2/3rd *precommits* where a common `SoftwareUpgradeProposal` is signaled, all the nodes (including validator nodes, non-validating full nodes and light-nodes) are expected to switch to the new version of the software.
|
||||
|
||||
*Note: Not clear how the flip is handled programatically*
|
||||
|
||||
|
||||
## Implementation
|
||||
|
||||
*Disclaimer: This is a suggestion. Only structs and pseudocode. Actual logic and implementation might widely differ*
|
||||
|
||||
### State
|
||||
|
||||
#### Procedures
|
||||
|
||||
`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 int64 // Minimum deposit for a proposal to enter voting period.
|
||||
OptionSet []string // Options available to voters. {Yes, No, NoWithVeto, Abstain}
|
||||
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.
|
||||
}
|
||||
```
|
||||
|
||||
**Store**:
|
||||
- `Procedures`: a mapping `map[int16]Procedure` of procedures indexed by their `ProcedureNumber`
|
||||
- `ActiveProcedureNumber`: returns current procedure number
|
||||
|
||||
#### Proposals
|
||||
|
||||
`Proposals` are item to be voted on.
|
||||
|
||||
```Go
|
||||
type Proposal struct {
|
||||
Title string // Title of the proposal
|
||||
Description string // Description of the proposal
|
||||
Type string // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal}
|
||||
Category bool // false=regular, true=urgent
|
||||
Deposit int64 // Current deposit on this proposal. Initial value is set at InitialDeposit
|
||||
SubmitBlock int64 // Height of the block where TxGovSubmitProposal was included
|
||||
|
||||
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)
|
||||
InitProcedureNumber int16 // Procedure number of the active procedure when proposal enters voting period (default -1)
|
||||
Votes map[string]int64 // Votes for each option (Yes, No, NoWithVeto, Abstain)
|
||||
}
|
||||
```
|
||||
|
||||
We also introduce a type `ValidatorGovInfo`
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
|
||||
**Store:**
|
||||
|
||||
- `Proposals`: A mapping `map[int64]Proposal` of proposals indexed by their `proposalID`
|
||||
- `Deposits`: A mapping `map[[]byte]int64` of deposits indexed by `<proposalID>:<depositorPubKey>` as `[]byte`. Given a `proposalID` and a `PubKey`, returns deposit (`nil` if `PubKey` has not deposited on the proposal)
|
||||
- `Options`: A mapping `map[[]byte]string` of options indexed by `<proposalID>:<voterPubKey>:<validatorPubKey>` as `[]byte`. Given a `proposalID`, a `PubKey` and a validator's `PubKey`, returns option chosen by this `PubKey` for this validator (`nil` if `PubKey` has not voted under this validator)
|
||||
- `ValidatorGovInfos`: A mapping `map[[]byte]ValidatorGovInfo` of validator's governance infos indexed by `<proposalID>:<validatorGovPubKey>`. Returns `nil` if proposal has not entered voting period or if `PubKey` was not the governance public key of a validator when proposal entered voting period.
|
||||
|
||||
|
||||
#### Proposal Processing Queue
|
||||
|
||||
**Store:**
|
||||
- `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 and, if not, applies `GovernancePenalty`. After that proposal is ejected from `ProposalProcessingQueue` and the next element of the queue is evaluated. Note that if a proposal is urgent and accepted under the special condition, its `ProposalID` must be ejected from `ProposalProcessingQueue`.
|
||||
|
||||
And the pseudocode for the `ProposalProcessingQueue`:
|
||||
|
||||
```
|
||||
in BeginBlock do
|
||||
|
||||
checkProposal() // First call of the recursive function
|
||||
|
||||
|
||||
// Recursive function. First call in BeginBlock
|
||||
func checkProposal()
|
||||
if (ProposalProcessingQueue.Peek() == nil)
|
||||
return
|
||||
|
||||
else
|
||||
proposalID = ProposalProcessingQueue.Peek()
|
||||
proposal = load(store, Proposals, proposalID)
|
||||
initProcedure = load(store, Procedures, proposal.InitProcedureNumber)
|
||||
|
||||
if (proposal.Category AND proposal.Votes['Yes']/proposal.InitTotalVotingPower >= 2/3)
|
||||
|
||||
// proposal was urgent and accepted under the special condition
|
||||
// no punishment
|
||||
|
||||
ProposalProcessingQueue.pop()
|
||||
checkProposal()
|
||||
|
||||
else if (CurrentBlock == proposal.VotingStartBlock + initProcedure.VotingPeriod)
|
||||
|
||||
activeProcedure = load(store, Procedures, ActiveProcedureNumber)
|
||||
|
||||
for each validator in CurrentBondedValidators
|
||||
validatorGovInfo = load(store, ValidatorGovInfos, validator.GovPubKey)
|
||||
|
||||
if (validatorGovInfo.InitVotingPower != nil)
|
||||
// validator was bonded when vote started
|
||||
|
||||
validatorOption = load(store, Options, validator.GovPubKey)
|
||||
if (validatorOption == nil)
|
||||
// validator did not vote
|
||||
slash validator by activeProcedure.GovernancePenalty
|
||||
|
||||
ProposalProcessingQueue.pop()
|
||||
checkProposal()
|
||||
```
|
||||
|
||||
|
||||
### Transactions
|
||||
|
||||
#### Proposal Submission
|
||||
|
||||
Proposals can be submitted by any Atom holder via a `TxGovSubmitProposal` transaction.
|
||||
|
||||
```Go
|
||||
type TxGovSubmitProposal struct {
|
||||
Title string // Title of the proposal
|
||||
Description string // Description of the proposal
|
||||
Type string // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal}
|
||||
Category bool // false=regular, true=urgent
|
||||
InitialDeposit int64 // Initial deposit paid by sender. Must be strictly positive.
|
||||
}
|
||||
```
|
||||
|
||||
**State modifications:**
|
||||
- Generate new `proposalID`
|
||||
- Create new `Proposal`
|
||||
- Initialise `Proposals` attributes
|
||||
- Store sender's deposit in `Deposits`
|
||||
- Decrease balance of sender by `InitialDeposit`
|
||||
- If `MinDeposit` is reached:
|
||||
- Push `proposalID` in `ProposalProcessingQueueEnd`
|
||||
- Store each validator's voting power in `ValidatorGovInfos`
|
||||
|
||||
A `TxGovSubmitProposal` transaction can be handled according to the following pseudocode
|
||||
|
||||
```
|
||||
// PSEUDOCODE //
|
||||
// Check if TxGovSubmitProposal is valid. If it is, create proposal //
|
||||
|
||||
upon receiving txGovSubmitProposal from sender do
|
||||
|
||||
if !correctlyFormatted(txGovSubmitProposal) then
|
||||
// check if proposal is correctly formatted. Includes fee payment.
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
if (txGovSubmitProposal.InitialDeposit <= 0) OR (sender.AtomBalance < InitialDeposit) then
|
||||
// InitialDeposit is negative or null OR sender has insufficient funds
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
sender.AtomBalance -= txGovSubmitProposal.InitialDeposit
|
||||
|
||||
proposalID = generate new proposalID
|
||||
proposal = NewProposal()
|
||||
|
||||
proposal.Title = txGovSubmitProposal.Title
|
||||
proposal.Description = txGovSubmitProposal.Description
|
||||
proposal.Type = txGovSubmitProposal.Type
|
||||
proposal.Category = txGovSubmitProposal.Category
|
||||
proposal.Deposit = txGovSubmitProposal.InitialDeposit
|
||||
proposal.SubmitBlock = CurrentBlock
|
||||
|
||||
store(Deposits, <proposalID>:<sender>, txGovSubmitProposal.InitialDeposit)
|
||||
activeProcedure = load(store, Procedures, ActiveProcedureNumber)
|
||||
|
||||
if (txGovSubmitProposal.InitialDeposit < activeProcedure.MinDeposit) then
|
||||
// MinDeposit is not reached
|
||||
|
||||
proposal.VotingStartBlock = -1
|
||||
proposal.InitTotalVotingPower = 0
|
||||
proposal.InitProcedureNumber = -1
|
||||
|
||||
else
|
||||
// MinDeposit is reached
|
||||
|
||||
proposal.VotingStartBlock = CurrentBlock
|
||||
proposal.InitTotalVotingPower = TotalVotingPower
|
||||
proposal.InitProcedureNumber = ActiveProcedureNumber
|
||||
|
||||
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.GovPubKey>, validatorGovInfo)
|
||||
|
||||
ProposalProcessingQueue.push(proposalID)
|
||||
|
||||
store(Proposals, proposalID, proposal) // Store proposal in Proposals mapping
|
||||
return proposalID
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### Deposit
|
||||
|
||||
Once a proposal is submitted, if `Proposal.Deposit < ActiveProcedure.MinDeposit`, Atom holders can send `TxGovDeposit` transactions to increase the proposal's deposit.
|
||||
|
||||
```Go
|
||||
type TxGovDeposit struct {
|
||||
ProposalID int64 // ID of the proposal
|
||||
Deposit int64 // Number of Atoms to add to the proposal's deposit
|
||||
}
|
||||
```
|
||||
|
||||
**State modifications:**
|
||||
- Decrease balance of sender by `deposit`
|
||||
- Initialize or increase `deposit` of sender in `Deposits`
|
||||
- Increase `proposal.Deposit` 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.
|
||||
|
||||
```
|
||||
// PSEUDOCODE //
|
||||
// Check if TxGovDeposit is valid. If it is, increase deposit and check if MinDeposit is reached
|
||||
|
||||
upon receiving txGovDeposit from sender do
|
||||
// check if proposal is correctly formatted. Includes fee payment.
|
||||
|
||||
if !correctlyFormatted(txGovDeposit) then
|
||||
throw
|
||||
|
||||
else
|
||||
proposal = load(store, Proposals, txGovDeposit.ProposalID)
|
||||
|
||||
if (proposal == nil) then
|
||||
// There is no proposal for this proposalID
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
if (txGovDeposit.Deposit <= 0) OR (sender.AtomBalance < txGovDeposit.Deposit)
|
||||
// deposit is negative or null OR sender has insufficient funds
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
activeProcedure = load(store, Procedures, ActiveProcedureNumber)
|
||||
if (proposal.Deposit >= activeProcedure.MinDeposit) then
|
||||
// MinDeposit was reached
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
if (CurrentBlock >= proposal.SubmitBlock + activeProcedure.MaxDepositPeriod) then
|
||||
// Maximum deposit period reached
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
// sender can deposit
|
||||
|
||||
sender.AtomBalance -= txGovDeposit.Deposit
|
||||
deposit = load(store, Deposits, <txGovDeposit.ProposalID>:<sender>)
|
||||
|
||||
if (deposit == nil)
|
||||
// sender has never deposited on this proposal
|
||||
|
||||
store(Deposits, <txGovDeposit.ProposalID>:<sender>, deposit)
|
||||
|
||||
else
|
||||
// sender has already deposited on this proposal
|
||||
|
||||
newDeposit = deposit + txGovDeposit.Deposit
|
||||
store(Deposits, <txGovDeposit.ProposalID>:<sender>, newDeposit)
|
||||
|
||||
proposal.Deposit += txGovDeposit.Deposit
|
||||
|
||||
if (proposal.Deposit >= activeProcedure.MinDeposit) then
|
||||
// MinDeposit is reached, vote opens
|
||||
|
||||
proposal.VotingStartBlock = CurrentBlock
|
||||
proposal.InitTotalVotingPower = TotalVotingPower
|
||||
proposal.InitProcedureNumber = ActiveProcedureNumber
|
||||
|
||||
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.GovPubKey>, validatorGovInfo)
|
||||
|
||||
ProposalProcessingQueue.push(txGovDeposit.ProposalID)
|
||||
```
|
||||
|
||||
#### Claiming deposit
|
||||
|
||||
Finally, if the proposal is accepted or `MinDeposit` was not reached before the end of the `MaximumDepositPeriod`, then Atom holders can send `TxGovClaimDeposit` transaction to claim their deposits.
|
||||
|
||||
```Go
|
||||
type TxGovClaimDeposit struct {
|
||||
ProposalID int64
|
||||
}
|
||||
```
|
||||
|
||||
**State modifications:**
|
||||
If conditions are met, reimburse the deposit, i.e.
|
||||
- Increase `AtomBalance` of sender by `deposit`
|
||||
- Set `deposit` of sender in `DepositorsList` to 0
|
||||
|
||||
And the associated pseudocode
|
||||
|
||||
```
|
||||
// PSEUDOCODE //
|
||||
/* Check if TxGovClaimDeposit is valid. If vote never started and MaxDepositPeriod is reached or if vote started and proposal was accepted, return deposit */
|
||||
|
||||
upon receiving txGovClaimDeposit from sender do
|
||||
// check if proposal is correctly formatted. Includes fee payment.
|
||||
|
||||
if !correctlyFormatted(txGovClaimDeposit) then
|
||||
throw
|
||||
|
||||
else
|
||||
proposal = load(store, Proposals, txGovDeposit.ProposalID)
|
||||
|
||||
if (proposal == nil) then
|
||||
// There is no proposal for this proposalID
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
deposit = load(store, Deposits, <txGovClaimDeposit.ProposalID>:<sender>)
|
||||
|
||||
if (deposit == nil)
|
||||
// sender has not deposited on this proposal
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
if (deposit <= 0)
|
||||
// deposit has already been claimed
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
if (proposal.VotingStartBlock <= 0)
|
||||
// Vote never started
|
||||
|
||||
activeProcedure = load(store, Procedures, ActiveProcedureNumber)
|
||||
if (CurrentBlock <= proposal.SubmitBlock + activeProcedure.MaxDepositPeriod)
|
||||
// MaxDepositPeriod is not reached
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
// MaxDepositPeriod is reached
|
||||
// Set sender's deposit to 0 and refund
|
||||
|
||||
store(Deposits, <txGovClaimDeposit.ProposalID>:<sender>, 0)
|
||||
sender.AtomBalance += deposit
|
||||
|
||||
else
|
||||
// Vote started
|
||||
|
||||
initProcedure = load(store, Procedures, proposal.InitProcedureNumber)
|
||||
|
||||
if (proposal.Category AND proposal.Votes['Yes']/proposal.InitTotalVotingPower >= 2/3) OR
|
||||
((CurrentBlock > proposal.VotingStartBlock + initProcedure.VotingPeriod) AND (proposal.Votes['NoWithVeto']/(proposal.Votes['Yes']+proposal.Votes['No']+proposal.Votes['NoWithVeto']) < 1/3) AND (proposal.Votes['Yes']/(proposal.Votes['Yes']+proposal.Votes['No']+proposal.Votes['NoWithVeto']) > 1/2)) then
|
||||
|
||||
// Proposal was accepted either because
|
||||
// Proposal was urgent and special condition was met
|
||||
// Voting period ended and vote satisfies threshold
|
||||
|
||||
store(Deposits, <txGovClaimDeposit.ProposalID>:<sender>, 0)
|
||||
sender.AtomBalance += deposit
|
||||
|
||||
```
|
||||
|
||||
|
||||
#### Vote
|
||||
|
||||
Once `ActiveProcedure.MinDeposit` is reached, voting period starts. From there, bonded Atom holders are able to send `TxGovVote` transactions to cast their vote on the proposal.
|
||||
|
||||
```Go
|
||||
type TxGovVote struct {
|
||||
ProposalID int64 // proposalID of the proposal
|
||||
Option string // option from OptionSet chosen by the voter
|
||||
ValidatorPubKey crypto.PubKey // PubKey of the validator voter wants to tie its vote to
|
||||
}
|
||||
```
|
||||
|
||||
**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 `proposal.Votes['validatorOption']` by sender's `voting power`
|
||||
- If sender is not a validator, increase `[proposal.Votes['txGovVote.Option']` by sender's `voting power`
|
||||
- If sender is a validator, increase `proposal.Votes['txGovVote.Option']` by validator's `InitialVotingPower - minus` (`minus` can be equal to 0)
|
||||
|
||||
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 GovernancePubKey as `ValidatorPubKey`
|
||||
|
||||
|
||||
|
||||
Next is a pseudocode proposal of the way `TxGovVote` transactions can be handled:
|
||||
|
||||
```
|
||||
// PSEUDOCODE //
|
||||
// Check if TxGovVote is valid. If it is, count vote//
|
||||
|
||||
upon receiving txGovVote from sender do
|
||||
// check if proposal is correctly formatted. Includes fee payment.
|
||||
|
||||
if !correctlyFormatted(txGovDeposit) then
|
||||
throw
|
||||
|
||||
else
|
||||
proposal = load(store, Proposals, txGovDeposit.ProposalID)
|
||||
|
||||
if (proposal == nil) then
|
||||
// There is no proposal for this proposalID
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
initProcedure = load(store, Procedures, proposal.InitProcedureNumber) // get procedure that was active when vote opened
|
||||
validator = load(store, Validators, txGovVote.ValidatorPubKey)
|
||||
|
||||
if !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
|
||||
// ValidatorPubKey is not the GovPubKey of a current validator
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
option = load(store, Options, <txGovVote.ProposalID>:<sender>:<txGovVote.ValidatorPubKey>)
|
||||
|
||||
if (option != nil)
|
||||
// sender has already voted with the Atoms bonded to ValidatorPubKey
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
if (proposal.VotingStartBlock < 0) OR
|
||||
(CurrentBlock > proposal.VotingStartBlock + initProcedure.VotingPeriod) OR
|
||||
(proposal.VotingStartBlock < lastBondingBlock(sender, txGovVote.ValidatorPubKey) OR
|
||||
(proposal.VotingStartBlock < lastUnbondingBlock(sender, txGovVote.ValidatorPubKey) OR
|
||||
(proposal.Category AND proposal.Votes['Yes']/proposal.InitTotalVotingPower >= 2/3) then
|
||||
|
||||
// Throws if
|
||||
// Vote has not started OR if
|
||||
// Vote had ended OR if
|
||||
// sender bonded Atoms to ValidatorPubKey after start of vote OR if
|
||||
// sender unbonded Atoms from ValidatorPubKey after start of vote OR if
|
||||
// proposal is urgent and special condition is met, i.e. proposal is accepted and closed
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
validatorGovInfo = load(store, ValidatorGovInfos, <txGovVote.ProposalID>:<validator.ValidatorGovPubKey>)
|
||||
|
||||
if (validatorGovInfo == nil)
|
||||
// validator became validator after proposal entered voting period
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
// sender can vote, check if sender == validator and store sender's option in Options
|
||||
|
||||
store(Options, <txGovVote.ProposalID>:<sender>:<txGovVote.ValidatorPubKey>, txGovVote.Option)
|
||||
|
||||
if (sender != validator.GovPubKey)
|
||||
// Here, sender is not the Governance PubKey of the validator whose PubKey is txGovVote.ValidatorPubKey
|
||||
|
||||
if sender does not have bonded Atoms to txGovVote.ValidatorPubKey then
|
||||
// check in Staking module
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
validatorOption = load(store, Options, <txGovVote.ProposalID>:<sender>:<txGovVote.ValidatorPubKey)
|
||||
|
||||
if (validatorOption == nil)
|
||||
// Validator has not voted already
|
||||
|
||||
validatorGovInfo.Minus += sender.bondedAmounTo(txGovVote.ValidatorPubKey)
|
||||
store(ValidatorGovInfos, <txGovVote.ProposalID>:<validator.ValidatorGovPubKey>, validatorGovInfo)
|
||||
|
||||
else
|
||||
// Validator has already voted
|
||||
// Reduce votes of option chosen by validator by sender's bonded Amount
|
||||
|
||||
proposal.Votes['validatorOption'] -= sender.bondedAmountTo(txGovVote.ValidatorPubKey)
|
||||
|
||||
// increase votes of option chosen by sender by bonded Amount
|
||||
proposal.Votes['txGovVote.Option'] += sender.bondedAmountTo(txGovVote.ValidatorPubKey)
|
||||
|
||||
else
|
||||
// sender is the Governance PubKey of the validator whose main PubKey is txGovVote.ValidatorPubKey
|
||||
// i.e. sender == validator
|
||||
|
||||
proposal.Votes['txGovVote.Option'] += (validatorGovInfo.InitVotingPower - validatorGovInfo.Minus)
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Future improvements (not in scope for MVP)
|
||||
|
||||
The current documentation only describes the minimum viable product for the governance module. Future improvements may include:
|
||||
|
||||
- **`BountyProposals`:** If accepted, a `BountyProposal` creates an open bounty. The `BountyProposal` specifies how many Atoms will be given upon completion. These Atoms will be taken from the `reserve pool`. After a `BountyProposal` is accepted by governance, anybody can submit a `SoftwareUpgradeProposal` with the code to claim the bounty. Note that once a `BountyProposal` is accepted, the corresponding funds in the `reserve pool` are locked so that payment can always be honored. In order to link a `SoftwareUpgradeProposal` to an open bounty, the submitter of the `SoftwareUpgradeProposal` will use the `Proposal.LinkedProposal` attribute. If a `SoftwareUpgradeProposal` linked to an open bounty is accepted by governance, the funds that were reserved are automatically transferred to the submitter.
|
||||
- **Complex delegation:** Delegators could choose other representatives than their validators. Ultimately, the chain of representatives would always end up to a validator, but delegators could inherit the vote of their chosen representative before they inherit the vote of their validator. In other words, they would only inherit the vote of their validator if their other appointed representative did not vote.
|
||||
- **`ParameterProposals` and `WhitelistProposals`:** These proposals would automatically change pre-defined parameters and whitelists. Upon acceptance, these proposals would not require validators to do the signal and switch process.
|
||||
- **Better process for proposal review:** There would be two parts to `proposal.Deposit`, one for anti-spam (same as in MVP) and an other one to reward third party auditors.
|
|
@ -10,11 +10,29 @@ 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 VoteType byte
|
||||
|
||||
const (
|
||||
VoteTypeYes = 0x1
|
||||
VoteTypeNo = 0x2
|
||||
VoteTypeNoWithVeto = 0x3
|
||||
VoteTypeAbstain = 0x4
|
||||
)
|
||||
|
||||
type ProposalType byte
|
||||
|
||||
const (
|
||||
ProposalTypePlainText = 0x1
|
||||
ProposalTypeSoftwareUpgrade = 0x2
|
||||
|
||||
)
|
||||
|
||||
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.
|
||||
OptionSet []string // Options available to voters. {Yes, No, NoWithVeto, Abstain}
|
||||
ProposalTypes []string // Types available to submitters. {PlainTextProposal, SoftwareUpgradeProposal}
|
||||
VoteTypes []VoteType // Vote types available to voters.
|
||||
ProposalTypes []ProposalType // Proposal types available to submitters.
|
||||
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
|
||||
|
@ -30,7 +48,7 @@ The current active procedure is stored in a global `params` KVStore.
|
|||
|
||||
```go
|
||||
type Deposit struct {
|
||||
Amount sdk.Coins // sAmount of coins deposited by depositer
|
||||
Amount sdk.Coins // Amount of coins deposited by depositer
|
||||
Depositer crypto.address // Address of depositer
|
||||
}
|
||||
```
|
||||
|
@ -39,27 +57,27 @@ The current active procedure is stored in a global `params` KVStore.
|
|||
|
||||
```go
|
||||
type Votes struct {
|
||||
YesVotes int64
|
||||
NoVote int64
|
||||
NoWithVetoVotes int64
|
||||
AbstainVotes int64
|
||||
Yes int64
|
||||
No int64
|
||||
NoWithVeto int64
|
||||
Abstain int64
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Proposals
|
||||
|
||||
`Proposals` are item to be voted on.
|
||||
`Proposals` are an item to be voted on.
|
||||
|
||||
```go
|
||||
type Proposal struct {
|
||||
Title string // Title of the proposal
|
||||
Description string // Description of the proposal
|
||||
Type string // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal}
|
||||
Type ProposalType // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal}
|
||||
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 crypto.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)
|
||||
|
@ -83,14 +101,12 @@ type ValidatorGovInfo struct {
|
|||
*Stores are KVStores in the multistore. The key to find the store is the first parameter in the list*
|
||||
|
||||
|
||||
* `Proposals`: A mapping `map[int64]Proposal` of proposals indexed by their
|
||||
* `Proposals: int64 => Proposal` maps `proposalID` to the `Proposal`
|
||||
`proposalID`
|
||||
* `Options`: A mapping `map[[]byte]string` of options indexed by
|
||||
`<proposalID>:<voterAddress>:<validatorAddress>` as `[]byte`. Given a
|
||||
`proposalID`, an `address` and a validator's `address`, returns option chosen by this `address` for this validator (`nil` if `address` has not voted under this validator)
|
||||
* `ValidatorGovInfos`: A mapping `map[[]byte]ValidatorGovInfo` of validator's
|
||||
governance infos indexed by `<proposalID>:<validatorAddress>`. Returns
|
||||
`nil` if proposal has not entered voting period or if `address` was not the
|
||||
* `Options: <proposalID | voterAddress | validatorAddress> => 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: <proposalID | validatorAddress> => 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,62 +137,62 @@ And the pseudocode for the `ProposalProcessingQueue`:
|
|||
|
||||
// Recursive function. First call in BeginBlock
|
||||
func checkProposal()
|
||||
if (ProposalProcessingQueue.Peek() == nil)
|
||||
proposalID = ProposalProcessingQueue.Peek()
|
||||
if (proposalID == nil)
|
||||
return
|
||||
|
||||
else
|
||||
proposalID = ProposalProcessingQueue.Peek()
|
||||
proposal = load(Proposals, proposalID)
|
||||
proposal = load(Proposals, proposalID)
|
||||
|
||||
if (proposal.Votes.YesVotes/proposal.InitTotalVotingPower >= 2/3)
|
||||
if (proposal.Votes.YesVotes/proposal.InitTotalVotingPower > 2/3)
|
||||
|
||||
// proposal was urgent and accepted under the special condition
|
||||
// no punishment
|
||||
// refund deposits
|
||||
// proposal accepted early by super-majority
|
||||
// no punishments; refund deposits
|
||||
|
||||
ProposalProcessingQueue.pop()
|
||||
ProposalProcessingQueue.pop()
|
||||
|
||||
newDeposits = new []Deposits
|
||||
var newDeposits []Deposits
|
||||
|
||||
for each (amount, depositer) in proposal.Deposits
|
||||
newDeposits.append[{0, depositer}]
|
||||
depositer.AtomBalance += amount
|
||||
// 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)
|
||||
proposal.Deposits = newDeposits
|
||||
store(Proposals, proposalID, proposal)
|
||||
|
||||
checkProposal()
|
||||
checkProposal()
|
||||
|
||||
else if (CurrentBlock == proposal.VotingStartBlock + proposal.Procedure.VotingPeriod)
|
||||
else if (CurrentBlock == proposal.VotingStartBlock + proposal.Procedure.VotingPeriod)
|
||||
|
||||
ProposalProcessingQueue.pop()
|
||||
activeProcedure = load(params, 'ActiveProcedure')
|
||||
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
|
||||
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><validator.address>)
|
||||
if (validatorOption == nil)
|
||||
// validator did not vote
|
||||
slash validator by activeProcedure.GovernancePenalty
|
||||
validatorOption = load(Options, <proposalID | validator.Address>)
|
||||
if (validatorOption == nil)
|
||||
// validator did not vote
|
||||
slash validator by activeProcedure.GovernancePenalty
|
||||
|
||||
|
||||
if((proposal.Votes.YesVotes/(proposal.Votes.YesVotes + proposal.Votes.NoVotes + proposal.Votes.NoWithVetoVotes)) > 0.5 AND (proposal.Votes.NoWithVetoVotes/(proposal.Votes.YesVotes + proposal.Votes.NoVotes + proposal.Votes.NoWithVetoVotes) < 1/3))
|
||||
totalNonAbstain = proposal.Votes.YesVotes + proposal.Votes.NoVotes + proposal.Votes.NoWithVetoVotes
|
||||
if( proposal.Votes.YesVotes/totalNonAbstain > 0.5 AND proposal.Votes.NoWithVetoVotes/totalNonAbstain < 1/3)
|
||||
|
||||
// proposal was accepted at the end of the voting period
|
||||
// refund deposits
|
||||
// refund deposits (non-voters already punished)
|
||||
|
||||
newDeposits = new []Deposits
|
||||
var newDeposits []Deposits
|
||||
|
||||
for each (amount, depositer) in proposal.Deposits
|
||||
newDeposits.append[{0, depositer}]
|
||||
depositer.AtomBalance += amount
|
||||
|
||||
proposal.Deposits = newDeposits
|
||||
store(Proposals, <proposalID>, proposal)
|
||||
store(Proposals, proposalID, proposal)
|
||||
|
||||
checkProposal()
|
||||
```
|
||||
```
|
||||
|
|
|
@ -11,7 +11,7 @@ transaction.
|
|||
type TxGovSubmitProposal struct {
|
||||
Title string // Title of the proposal
|
||||
Description string // Description of the proposal
|
||||
Type string // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal}
|
||||
Type ProposalType // Type of proposal
|
||||
InitialDeposit int64 // Initial deposit paid by sender. Must be strictly positive.
|
||||
}
|
||||
```
|
||||
|
@ -36,61 +36,58 @@ upon receiving txGovSubmitProposal from sender do
|
|||
|
||||
if !correctlyFormatted(txGovSubmitProposal) then
|
||||
// check if proposal is correctly formatted. Includes fee payment.
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
if (txGovSubmitProposal.InitialDeposit <= 0) OR (sender.AtomBalance < InitialDeposit) then
|
||||
// InitialDeposit is negative or null OR sender has insufficient funds
|
||||
|
||||
throw
|
||||
initialDeposit = txGovSubmitProposal.InitialDeposit
|
||||
if (initialDeposit <= 0) OR (sender.AtomBalance < initialDeposit) then
|
||||
// InitialDeposit is negative or null OR sender has insufficient funds
|
||||
throw
|
||||
|
||||
sender.AtomBalance -= initialDeposit
|
||||
|
||||
proposalID = generate new proposalID
|
||||
proposal = NewProposal()
|
||||
|
||||
proposal.Title = txGovSubmitProposal.Title
|
||||
proposal.Description = txGovSubmitProposal.Description
|
||||
proposal.Type = txGovSubmitProposal.Type
|
||||
proposal.TotalDeposit = initialDeposit
|
||||
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
|
||||
|
||||
activeProcedure = load(params, 'ActiveProcedure')
|
||||
|
||||
if (initialDeposit < activeProcedure.MinDeposit) then
|
||||
// MinDeposit is not reached
|
||||
|
||||
else
|
||||
sender.AtomBalance -= txGovSubmitProposal.InitialDeposit
|
||||
|
||||
proposalID = generate new proposalID
|
||||
proposal = NewProposal()
|
||||
|
||||
proposal.Title = txGovSubmitProposal.Title
|
||||
proposal.Description = txGovSubmitProposal.Description
|
||||
proposal.Type = txGovSubmitProposal.Type
|
||||
proposal.TotalDeposit = txGovSubmitProposal.InitialDeposit
|
||||
proposal.SubmitBlock = CurrentBlock
|
||||
proposal.Deposits.append({InitialDeposit, sender})
|
||||
proposal.Submitter = sender
|
||||
proposal.Votes.YesVotes = 0
|
||||
proposal.Votes.NoVotes = 0
|
||||
proposal.Votes.NoWithVetoVotes = 0
|
||||
proposal.Votes.AbstainVotes = 0
|
||||
|
||||
activeProcedure = load(params, 'ActiveProcedure')
|
||||
proposal.VotingStartBlock = -1
|
||||
proposal.InitTotalVotingPower = 0
|
||||
|
||||
if (txGovSubmitProposal.InitialDeposit < activeProcedure.MinDeposit) then
|
||||
// MinDeposit is not reached
|
||||
|
||||
proposal.VotingStartBlock = -1
|
||||
proposal.InitTotalVotingPower = 0
|
||||
|
||||
else
|
||||
// MinDeposit is reached
|
||||
|
||||
proposal.VotingStartBlock = CurrentBlock
|
||||
proposal.InitTotalVotingPower = TotalVotingPower
|
||||
proposal.InitProcedure = activeProcedure
|
||||
|
||||
for each validator in CurrentBondedValidators
|
||||
// Store voting power of each bonded validator
|
||||
else
|
||||
// MinDeposit is reached
|
||||
|
||||
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
|
||||
validatorGovInfo = new ValidatorGovInfo
|
||||
validatorGovInfo.InitVotingPower = validator.VotingPower
|
||||
validatorGovInfo.Minus = 0
|
||||
|
||||
store(ValidatorGovInfos, <proposalID>:<validator.Address>, validatorGovInfo)
|
||||
|
||||
ProposalProcessingQueue.push(proposalID)
|
||||
store(ValidatorGovInfos, <proposalID | validator.Address>, validatorGovInfo)
|
||||
|
||||
ProposalProcessingQueue.push(proposalID)
|
||||
|
||||
store(Proposals, proposalID, proposal) // Store proposal in Proposals mapping
|
||||
return proposalID
|
||||
store(Proposals, proposalID, proposal) // Store proposal in Proposals mapping
|
||||
return proposalID
|
||||
```
|
||||
|
||||
### Deposit
|
||||
|
@ -127,61 +124,54 @@ upon receiving txGovDeposit from sender do
|
|||
if !correctlyFormatted(txGovDeposit) then
|
||||
throw
|
||||
|
||||
else
|
||||
proposal = load(Proposals, txGovDeposit.ProposalID)
|
||||
proposal = load(Proposals, txGovDeposit.ProposalID)
|
||||
|
||||
if (proposal == nil) then
|
||||
// There is no proposal for this proposalID
|
||||
|
||||
if (proposal == nil) then
|
||||
// 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 (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
|
||||
|
||||
else
|
||||
if (txGovDeposit.Deposit <= 0) OR (sender.AtomBalance < txGovDeposit.Deposit)
|
||||
// deposit is negative or null OR sender has insufficient funds
|
||||
|
||||
throw
|
||||
// sender can deposit
|
||||
|
||||
sender.AtomBalance -= txGovDeposit.Deposit
|
||||
|
||||
proposal.Deposits.append({txGovVote.Deposit, sender})
|
||||
proposal.TotalDeposit += txGovDeposit.Deposit
|
||||
|
||||
if (proposal.TotalDeposit >= activeProcedure.MinDeposit) then
|
||||
// MinDeposit is reached, vote opens
|
||||
|
||||
else
|
||||
activeProcedure = load(params, 'ActiveProcedure')
|
||||
proposal.VotingStartBlock = CurrentBlock
|
||||
proposal.InitTotalVotingPower = TotalVotingPower
|
||||
proposal.InitProcedure = activeProcedure
|
||||
|
||||
for each validator in CurrentBondedValidators
|
||||
// Store voting power of each bonded validator
|
||||
|
||||
if (proposal.TotalDeposit >= activeProcedure.MinDeposit) then
|
||||
// MinDeposit was reached
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
if (CurrentBlock >= proposal.SubmitBlock + activeProcedure.MaxDepositPeriod) then
|
||||
// Maximum deposit period reached
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
// sender can deposit
|
||||
|
||||
sender.AtomBalance -= txGovDeposit.Deposit
|
||||
validatorGovInfo = NewValidatorGovInfo()
|
||||
validatorGovInfo.InitVotingPower = validator.VotingPower
|
||||
validatorGovInfo.Minus = 0
|
||||
|
||||
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
|
||||
store(ValidatorGovInfos, <proposalID | validator.Address>, validatorGovInfo)
|
||||
|
||||
ProposalProcessingQueue.push(txGovDeposit.ProposalID)
|
||||
|
||||
validatorGovInfo = NewValidatorGovInfo()
|
||||
validatorGovInfo.InitVotingPower = validator.VotingPower
|
||||
validatorGovInfo.Minus = 0
|
||||
|
||||
store(ValidatorGovInfos, <proposalID>:<validator.Address>, validatorGovInfo)
|
||||
|
||||
ProposalProcessingQueue.push(txGovDeposit.ProposalID)
|
||||
|
||||
store(Proposals, txGovVote.ProposalID, proposal)
|
||||
store(Proposals, txGovVote.ProposalID, proposal)
|
||||
```
|
||||
|
||||
### Vote
|
||||
|
@ -227,101 +217,88 @@ handled:
|
|||
if !correctlyFormatted(txGovDeposit) then
|
||||
throw
|
||||
|
||||
else
|
||||
proposal = load(Proposals, txGovDeposit.ProposalID)
|
||||
proposal = load(Proposals, txGovDeposit.ProposalID)
|
||||
|
||||
if (proposal == nil) then
|
||||
// There is no proposal for this proposalID
|
||||
|
||||
if (proposal == nil) then
|
||||
// There is no proposal for this proposalID
|
||||
throw
|
||||
|
||||
validator = load(CurrentValidators, txGovVote.ValidatorAddress)
|
||||
|
||||
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, <txGovVote.ProposalID>:<sender>:<txGovVote.ValidatorAddress>)
|
||||
|
||||
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, <txGovVote.ProposalID>:<validator.ValidatorAddress>)
|
||||
|
||||
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.ValidatorAddress>, 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, <txGovVote.ProposalID>:<sender>:<txGovVote.ValidatorAddress>)
|
||||
|
||||
if (validatorOption == nil)
|
||||
// Validator has not voted already
|
||||
|
||||
validatorGovInfo.Minus += sender.bondedAmounTo(txGovVote.ValidatorAddress)
|
||||
store(ValidatorGovInfos, <txGovVote.ProposalID>:<validator.ValidatorAddress>, validatorGovInfo)
|
||||
|
||||
else
|
||||
validator = load(CurrentValidators, txGovVote.ValidatorAddress)
|
||||
|
||||
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
|
||||
|
||||
else
|
||||
option = load(Options, <txGovVote.ProposalID>:<sender>:<txGovVote.ValidatorAddress>)
|
||||
// Validator has already voted
|
||||
// Reduce votes of option chosen by validator by sender's bonded Amount
|
||||
|
||||
if (option != nil)
|
||||
// sender has already voted with the Atoms bonded to ValidatorAddress
|
||||
proposal.Votes.validatorOption -= sender.bondedAmountTo(txGovVote.ValidatorAddress)
|
||||
|
||||
throw
|
||||
// increase votes of option chosen by sender by bonded Amount
|
||||
|
||||
else
|
||||
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
|
||||
senderOption = txGovVote.Option
|
||||
propoal.Votes.senderOption -= sender.bondedAmountTo(txGovVote.ValidatorAddress)
|
||||
|
||||
// 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
|
||||
store(Proposals, txGovVote.ProposalID, proposal)
|
||||
|
||||
|
||||
throw
|
||||
else
|
||||
// sender is the address of the validator whose main Address is txGovVote.ValidatorAddress
|
||||
// i.e. sender == validator
|
||||
|
||||
else
|
||||
validatorGovInfo = load(ValidatorGovInfos, <txGovVote.ProposalID>:<validator.ValidatorAddress>)
|
||||
proposal.Votes.validatorOption += (validatorGovInfo.InitVotingPower - validatorGovInfo.Minus)
|
||||
|
||||
if (validatorGovInfo == nil)
|
||||
// validator became validator after proposal entered voting period
|
||||
|
||||
throw
|
||||
|
||||
else
|
||||
// sender can vote, check if sender == validator and store sender's option in Options
|
||||
|
||||
store(Options, <txGovVote.ProposalID>:<sender>:<txGovVote.ValidatorAddress>, 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
|
||||
|
||||
else
|
||||
validatorOption = load(Options, <txGovVote.ProposalID>:<sender>:<txGovVote.ValidatorAddress>)
|
||||
|
||||
if (validatorOption == nil)
|
||||
// Validator has not voted already
|
||||
|
||||
validatorGovInfo.Minus += sender.bondedAmounTo(txGovVote.ValidatorAddress)
|
||||
store(ValidatorGovInfos, <txGovVote.ProposalID>:<validator.ValidatorAddress>, 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)
|
||||
|
||||
|
||||
```
|
||||
store(Proposals, txGovVote.ProposalID, proposal)
|
||||
```
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# Staking module specification
|
||||
|
||||
## Abstract
|
||||
|
||||
This paper specifies the Staking module of the Cosmos-SDK, which was first described in the [Cosmos Whitepaper](https://cosmos.network/about/whitepaper) in June 2016.
|
||||
|
||||
The module enables Cosmos-SDK based blockchain to support an advanced Proof-of-Stake system. In this system, holders of the native staking token of the chain can become candidate validators and can delegate tokens to candidate validators, ultimately determining the effective validator set for the system.
|
||||
|
||||
This module will be used in the Cosmos Hub, the first Hub in the Cosmos network.
|
||||
|
||||
## Contents
|
||||
|
||||
The following specification uses *Atom* as the native staking token. The module can be adapted to any Proof-Of-Stake blockchain by replacing *Atom* with the native staking token of the chain.
|
||||
|
||||
1. **[Design overview](overview.md)**
|
||||
2. **Implementation**
|
||||
1. **[State](state.md)**
|
||||
1. Global State
|
||||
2. Validator Candidates
|
||||
3. Delegator Bonds
|
||||
4. Unbond and Rebond Queue
|
||||
2. **[Transactions](transactions.md)**
|
||||
1. Declare Candidacy
|
||||
2. Edit Candidacy
|
||||
3. Delegate
|
||||
4. Unbond
|
||||
5. Redelegate
|
||||
6. ProveLive
|
||||
3. **[Validator Set Changes](valset-changes.md)**
|
||||
1. Validator set updates
|
||||
2. Slashing
|
||||
3. Automatic Unbonding
|
||||
3. **[Future improvements](future_improvements.md)**
|
|
@ -1,5 +1,38 @@
|
|||
# Staking Module
|
||||
|
||||
## Overview
|
||||
|
||||
The Cosmos Hub is a Tendermint-based Proof of Stake blockchain system that
|
||||
serves as a backbone of the Cosmos ecosystem. It is operated and secured by an
|
||||
open and globally decentralized set of validators. Tendermint consensus is a
|
||||
Byzantine fault-tolerant distributed protocol that involves all validators in
|
||||
the process of exchanging protocol messages in the production of each block. To
|
||||
avoid Nothing-at-Stake problem, a validator in Tendermint needs to lock up
|
||||
coins in a bond deposit. Tendermint protocol messages are signed by the
|
||||
validator's private key, and this is a basis for Tendermint strict
|
||||
accountability that allows punishing misbehaving validators by slashing
|
||||
(burning) their bonded Atoms. On the other hand, validators are rewarded for
|
||||
their service of securing blockchain network by the inflationary provisions and
|
||||
transactions fees. This incentives correct behavior of the validators and
|
||||
provides the economic security of the network.
|
||||
|
||||
The native token of the Cosmos Hub is called Atom; becoming a validator of the
|
||||
Cosmos Hub requires holding Atoms. However, not all Atom holders are validators
|
||||
of the Cosmos Hub. More precisely, there is a selection process that determines
|
||||
the validator set as a subset of all validator candidates (Atom holders that
|
||||
wants to become a validator). The other option for Atom holder is to delegate
|
||||
their atoms to validators, i.e., being a delegator. A delegator is an Atom
|
||||
holder that has bonded its Atoms by delegating it to a validator (or validator
|
||||
candidate). By bonding Atoms to secure the network (and taking a risk of being
|
||||
slashed in case of misbehaviour), a user is rewarded with inflationary
|
||||
provisions and transaction fees proportional to the amount of its bonded Atoms.
|
||||
The Cosmos Hub is designed to efficiently facilitate a small numbers of
|
||||
validators (hundreds), and large numbers of delegators (tens of thousands).
|
||||
More precisely, it is the role of the Staking module of the Cosmos Hub to
|
||||
support various staking functionality including validator set selection,
|
||||
delegating, bonding and withdrawing Atoms, and the distribution of inflationary
|
||||
provisions and transaction fees.
|
||||
|
||||
## Basic Terms and Definitions
|
||||
|
||||
* Cosmsos Hub - a Tendermint-based Proof of Stake blockchain system
|
||||
|
@ -179,7 +212,3 @@ provisions cycle:
|
|||
```go
|
||||
GlobalState.BondedPool += provisionTokensHourly
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
|
||||
# Slashing
|
||||
|
||||
A validator bond is an economic commitment made by a validator signing key to both the safety and liveness of
|
||||
the consensus. Validator keys must not sign invalid messages which could
|
||||
violate consensus safety, and their signed precommit messages must be regularly included in
|
||||
block commits.
|
||||
|
||||
The incentivization of these two goals are treated separately.
|
||||
|
||||
## Safety
|
||||
|
||||
Messges which may compromise the safety of the underlying consensus protocol ("equivocations")
|
||||
result in some amount of the offending validator's shares being removed ("slashed").
|
||||
|
||||
Currently, such messages include only the following:
|
||||
|
||||
- prevotes by the same validator for more than one BlockID at the same
|
||||
Height and Round
|
||||
- precommits by the same validator for more than one BlockID at the same
|
||||
Height and Round
|
||||
|
||||
We call any such pair of conflicting votes `Evidence`. Full nodes in the network prioritize the
|
||||
detection and gossipping of `Evidence` so that it may be rapidly included in blocks and the offending
|
||||
validators punished.
|
||||
|
||||
For some `evidence` to be valid, it must satisfy:
|
||||
|
||||
`evidence.Height >= block.Height - MAX_EVIDENCE_AGE`
|
||||
|
||||
If valid evidence is included in a block, the offending validator loses
|
||||
a constant `SLASH_PROPORTION` of their current stake:
|
||||
|
||||
```
|
||||
oldShares = validator.shares
|
||||
validator.shares = oldShares * (1 - SLASH_PROPORTION)
|
||||
```
|
||||
|
||||
This ensures that offending validators are punished the same amount whether they
|
||||
act as a single validator with X stake or as N validators with collectively X
|
||||
stake.
|
||||
|
||||
|
||||
|
||||
## Liveness
|
||||
|
||||
Every block includes a set of precommits by the validators for the previous block,
|
||||
known as the LastCommit. A LastCommit is valid so long as it contains precommits from +2/3 of voting power.
|
||||
|
||||
Proposers are incentivized to include precommits from all
|
||||
validators in the LastCommit by receiving additional fees
|
||||
proportional to the difference between the voting power included in the
|
||||
LastCommit and +2/3 (see [TODO](https://github.com/cosmos/cosmos-sdk/issues/967)).
|
||||
|
||||
Validators are penalized for failing to be included in the LastCommit for some
|
||||
number of blocks by being automatically unbonded.
|
||||
|
||||
|
||||
TODO: do we do this by trying to track absence directly in the state, using
|
||||
something like the below, or do we let users notify the app when a validator has
|
||||
been absent using the
|
||||
[TxLivenessCheck](https://github.com/cosmos/cosmos-sdk/blob/develop/docs/spec/staking/spec-technical.md#txlivelinesscheck).
|
||||
|
||||
|
||||
A list, `ValidatorAbsenceInfos`, is stored in the state and used to track how often
|
||||
validators were included in a LastCommit.
|
||||
|
||||
```go
|
||||
// Ordered by ValidatorAddress.
|
||||
// One entry for each validator.
|
||||
type ValidatorAbsenceInfos []ValidatorAbsenceInfo
|
||||
|
||||
type ValidatorAbsenceInfo struct {
|
||||
ValidatorAddress []byte // address of the validator
|
||||
FirstHeight int64 // first height the validator was absent
|
||||
Count int64 // number of heights validator was absent since (and including) first
|
||||
}
|
||||
```
|
||||
|
|
@ -1,622 +0,0 @@
|
|||
# Staking Module
|
||||
|
||||
## Overview
|
||||
|
||||
The Cosmos Hub is a Tendermint-based Proof of Stake blockchain system that
|
||||
serves as a backbone of the Cosmos ecosystem. It is operated and secured by an
|
||||
open and globally decentralized set of validators. Tendermint consensus is a
|
||||
Byzantine fault-tolerant distributed protocol that involves all validators in
|
||||
the process of exchanging protocol messages in the production of each block. To
|
||||
avoid Nothing-at-Stake problem, a validator in Tendermint needs to lock up
|
||||
coins in a bond deposit. Tendermint protocol messages are signed by the
|
||||
validator's private key, and this is a basis for Tendermint strict
|
||||
accountability that allows punishing misbehaving validators by slashing
|
||||
(burning) their bonded Atoms. On the other hand, validators are rewarded for
|
||||
their service of securing blockchain network by the inflationary provisions and
|
||||
transactions fees. This incentives correct behavior of the validators and
|
||||
provides the economic security of the network.
|
||||
|
||||
The native token of the Cosmos Hub is called Atom; becoming a validator of the
|
||||
Cosmos Hub requires holding Atoms. However, not all Atom holders are validators
|
||||
of the Cosmos Hub. More precisely, there is a selection process that determines
|
||||
the validator set as a subset of all validator candidates (Atom holders that
|
||||
wants to become a validator). The other option for Atom holder is to delegate
|
||||
their atoms to validators, i.e., being a delegator. A delegator is an Atom
|
||||
holder that has bonded its Atoms by delegating it to a validator (or validator
|
||||
candidate). By bonding Atoms to secure the network (and taking a risk of being
|
||||
slashed in case of misbehaviour), a user is rewarded with inflationary
|
||||
provisions and transaction fees proportional to the amount of its bonded Atoms.
|
||||
The Cosmos Hub is designed to efficiently facilitate a small numbers of
|
||||
validators (hundreds), and large numbers of delegators (tens of thousands).
|
||||
More precisely, it is the role of the Staking module of the Cosmos Hub to
|
||||
support various staking functionality including validator set selection,
|
||||
delegating, bonding and withdrawing Atoms, and the distribution of inflationary
|
||||
provisions and transaction fees.
|
||||
|
||||
## State
|
||||
|
||||
The staking module persists the following information to the store:
|
||||
* `GlobalState`, a struct describing the global pools, inflation, and
|
||||
fees
|
||||
* `ValidatorCandidates: <pubkey | shares> => <candidate>`, a map of all candidates (including current validators) in the store,
|
||||
indexed by their public key and shares in the global pool.
|
||||
* `DelegatorBonds: < delegator-address | candidate-pubkey > => <delegator-bond>`. a map of all delegations by a delegator to a candidate,
|
||||
indexed by delegator address and candidate pubkey.
|
||||
public key
|
||||
* `UnbondQueue`, the queue of unbonding delegations
|
||||
* `RedelegateQueue`, the queue of re-delegations
|
||||
|
||||
### Global State
|
||||
|
||||
The GlobalState contains information about the total amount of Atoms, the
|
||||
global bonded/unbonded position, the Atom inflation rate, and the fees.
|
||||
|
||||
`Params` is global data structure that stores system parameters and defines overall functioning of the
|
||||
module.
|
||||
|
||||
``` go
|
||||
type GlobalState struct {
|
||||
TotalSupply int64 // total supply of Atoms
|
||||
BondedPool int64 // reserve of bonded tokens
|
||||
BondedShares rational.Rat // sum of all shares distributed for the BondedPool
|
||||
UnbondedPool int64 // reserve of unbonding tokens held with candidates
|
||||
UnbondedShares rational.Rat // sum of all shares distributed for the UnbondedPool
|
||||
InflationLastTime int64 // timestamp of last processing of inflation
|
||||
Inflation rational.Rat // current annual inflation rate
|
||||
DateLastCommissionReset int64 // unix timestamp for last commission accounting reset
|
||||
FeePool coin.Coins // fee pool for all the fee shares which have already been distributed
|
||||
ReservePool coin.Coins // pool of reserve taxes collected on all fees for governance use
|
||||
Adjustment rational.Rat // Adjustment factor for calculating global fee accum
|
||||
}
|
||||
|
||||
type Params struct {
|
||||
HoldBonded Address // account where all bonded coins are held
|
||||
HoldUnbonding Address // account where all delegated but unbonding coins are held
|
||||
|
||||
InflationRateChange rational.Rational // maximum annual change in inflation rate
|
||||
InflationMax rational.Rational // maximum inflation rate
|
||||
InflationMin rational.Rational // minimum inflation rate
|
||||
GoalBonded rational.Rational // Goal of percent bonded atoms
|
||||
ReserveTax rational.Rational // Tax collected on all fees
|
||||
|
||||
MaxVals uint16 // maximum number of validators
|
||||
AllowedBondDenom string // bondable coin denomination
|
||||
|
||||
// gas costs for txs
|
||||
GasDeclareCandidacy int64
|
||||
GasEditCandidacy int64
|
||||
GasDelegate int64
|
||||
GasRedelegate int64
|
||||
GasUnbond int64
|
||||
}
|
||||
```
|
||||
|
||||
### Candidate
|
||||
|
||||
The `Candidate` holds the current state and some historical
|
||||
actions of validators or candidate-validators.
|
||||
|
||||
``` go
|
||||
type Candidate struct {
|
||||
Status CandidateStatus
|
||||
ConsensusPubKey crypto.PubKey
|
||||
GovernancePubKey crypto.PubKey
|
||||
Owner crypto.Address
|
||||
GlobalStakeShares rational.Rat
|
||||
IssuedDelegatorShares rational.Rat
|
||||
RedelegatingShares rational.Rat
|
||||
VotingPower rational.Rat
|
||||
Commission rational.Rat
|
||||
CommissionMax rational.Rat
|
||||
CommissionChangeRate rational.Rat
|
||||
CommissionChangeToday rational.Rat
|
||||
ProposerRewardPool coin.Coins
|
||||
Adjustment rational.Rat
|
||||
Description Description
|
||||
}
|
||||
|
||||
type Description struct {
|
||||
Name string
|
||||
DateBonded string
|
||||
Identity string
|
||||
Website string
|
||||
Details string
|
||||
}
|
||||
```
|
||||
|
||||
Candidate parameters are described:
|
||||
* Status: it can be Bonded (active validator), Unbonding (validator candidate)
|
||||
or Revoked
|
||||
* ConsensusPubKey: candidate public key that is used strictly for participating in
|
||||
consensus
|
||||
* GovernancePubKey: public key used by the validator for governance voting
|
||||
* Owner: Address that is allowed to unbond coins.
|
||||
* GlobalStakeShares: Represents shares of `GlobalState.BondedPool` if
|
||||
`Candidate.Status` is `Bonded`; or shares of `GlobalState.Unbondingt Pool`
|
||||
otherwise
|
||||
* IssuedDelegatorShares: Sum of all shares a candidate issued to delegators
|
||||
(which includes the candidate's self-bond); a delegator share represents
|
||||
their stake in the Candidate's `GlobalStakeShares`
|
||||
* RedelegatingShares: The portion of `IssuedDelegatorShares` which are
|
||||
currently re-delegating to a new validator
|
||||
* VotingPower: Proportional to the amount of bonded tokens which the validator
|
||||
has if `Candidate.Status` is `Bonded`; otherwise it is equal to `0`
|
||||
* Commission: The commission rate of fees charged to any delegators
|
||||
* CommissionMax: The maximum commission rate this candidate can charge each
|
||||
day from the date `GlobalState.DateLastCommissionReset`
|
||||
* CommissionChangeRate: The maximum daily increase of the candidate commission
|
||||
* CommissionChangeToday: Counter for the amount of change to commission rate
|
||||
which has occurred today, reset on the first block of each day (UTC time)
|
||||
* ProposerRewardPool: reward pool for extra fees collected when this candidate
|
||||
is the proposer of a block
|
||||
* Adjustment factor used to passively calculate each validators entitled fees
|
||||
from `GlobalState.FeePool`
|
||||
* Description
|
||||
* Name: moniker
|
||||
* DateBonded: date determined which the validator was bonded
|
||||
* Identity: optional field to provide a signature which verifies the
|
||||
validators identity (ex. UPort or Keybase)
|
||||
* Website: optional website link
|
||||
* Details: optional details
|
||||
|
||||
### DelegatorBond
|
||||
|
||||
Atom holders may delegate coins to candidates; under this circumstance their
|
||||
funds are held in a `DelegatorBond` data structure. It is owned by one
|
||||
delegator, and is associated with the shares for one candidate. The sender of
|
||||
the transaction is the owner of the bond.
|
||||
|
||||
``` go
|
||||
type DelegatorBond struct {
|
||||
Candidate crypto.PubKey
|
||||
Shares rational.Rat
|
||||
AdjustmentFeePool coin.Coins
|
||||
AdjustmentRewardPool coin.Coins
|
||||
}
|
||||
```
|
||||
|
||||
Description:
|
||||
* Candidate: the public key of the validator candidate: bonding too
|
||||
* Shares: the number of delegator shares received from the validator candidate
|
||||
* AdjustmentFeePool: Adjustment factor used to passively calculate each bonds
|
||||
entitled fees from `GlobalState.FeePool`
|
||||
* AdjustmentRewardPool: Adjustment factor used to passively calculate each
|
||||
bonds entitled fees from `Candidate.ProposerRewardPool`
|
||||
|
||||
|
||||
### QueueElem
|
||||
|
||||
The Unbonding and re-delegation process is implemented using the ordered queue
|
||||
data structure. All queue elements share a common structure:
|
||||
|
||||
```golang
|
||||
type QueueElem struct {
|
||||
Candidate crypto.PubKey
|
||||
InitTime int64 // when the element was added to the queue
|
||||
}
|
||||
```
|
||||
|
||||
The queue is ordered so the next element to unbond/re-delegate is at the head.
|
||||
Every tick the head of the queue is checked and if the unbonding period has
|
||||
passed since `InitTime`, the final settlement of the unbonding is started or
|
||||
re-delegation is executed, and the element is popped from the queue. Each
|
||||
`QueueElem` is persisted in the store until it is popped from the queue.
|
||||
|
||||
### QueueElemUnbondDelegation
|
||||
|
||||
QueueElemUnbondDelegation structure is used in the unbonding queue.
|
||||
|
||||
```golang
|
||||
type QueueElemUnbondDelegation struct {
|
||||
QueueElem
|
||||
Payout Address // account to pay out to
|
||||
Tokens coin.Coins // the value in Atoms of the amount of delegator shares which are unbonding
|
||||
StartSlashRatio rational.Rat // candidate slash ratio
|
||||
}
|
||||
```
|
||||
|
||||
### QueueElemReDelegate
|
||||
|
||||
QueueElemReDelegate structure is used in the re-delegation queue.
|
||||
|
||||
```golang
|
||||
type QueueElemReDelegate struct {
|
||||
QueueElem
|
||||
Payout Address // account to pay out to
|
||||
Shares rational.Rat // amount of shares which are unbonding
|
||||
NewCandidate crypto.PubKey // validator to bond to after unbond
|
||||
}
|
||||
```
|
||||
|
||||
### Transaction Overview
|
||||
|
||||
Available Transactions:
|
||||
* TxDeclareCandidacy
|
||||
* TxEditCandidacy
|
||||
* TxDelegate
|
||||
* TxUnbond
|
||||
* TxRedelegate
|
||||
* TxLivelinessCheck
|
||||
* TxProveLive
|
||||
|
||||
## Transaction processing
|
||||
|
||||
In this section we describe the processing of the transactions and the
|
||||
corresponding updates to the global state. In the following text we will use
|
||||
`gs` to refer to the `GlobalState` data structure, `unbondDelegationQueue` is a
|
||||
reference to the queue of unbond delegations, `reDelegationQueue` is the
|
||||
reference for the queue of redelegations. We use `tx` to denote a
|
||||
reference to a transaction that is being processed, and `sender` to denote the
|
||||
address of the sender of the transaction. We use function
|
||||
`loadCandidate(store, PubKey)` to obtain a Candidate structure from the store,
|
||||
and `saveCandidate(store, candidate)` to save it. Similarly, we use
|
||||
`loadDelegatorBond(store, sender, PubKey)` to load a delegator bond with the
|
||||
key (sender and PubKey) from the store, and
|
||||
`saveDelegatorBond(store, sender, bond)` to save it.
|
||||
`removeDelegatorBond(store, sender, bond)` is used to remove the bond from the
|
||||
store.
|
||||
|
||||
### TxDeclareCandidacy
|
||||
|
||||
A validator candidacy is declared using the `TxDeclareCandidacy` transaction.
|
||||
|
||||
```golang
|
||||
type TxDeclareCandidacy struct {
|
||||
ConsensusPubKey crypto.PubKey
|
||||
Amount coin.Coin
|
||||
GovernancePubKey crypto.PubKey
|
||||
Commission rational.Rat
|
||||
CommissionMax int64
|
||||
CommissionMaxChange int64
|
||||
Description Description
|
||||
}
|
||||
|
||||
declareCandidacy(tx TxDeclareCandidacy):
|
||||
candidate = loadCandidate(store, tx.PubKey)
|
||||
if candidate != nil return // candidate with that public key already exists
|
||||
|
||||
candidate = NewCandidate(tx.PubKey)
|
||||
candidate.Status = Unbonded
|
||||
candidate.Owner = sender
|
||||
init candidate VotingPower, GlobalStakeShares, IssuedDelegatorShares, RedelegatingShares and Adjustment to rational.Zero
|
||||
init commision related fields based on the values from tx
|
||||
candidate.ProposerRewardPool = Coin(0)
|
||||
candidate.Description = tx.Description
|
||||
|
||||
saveCandidate(store, candidate)
|
||||
|
||||
txDelegate = TxDelegate(tx.PubKey, tx.Amount)
|
||||
return delegateWithCandidate(txDelegate, candidate)
|
||||
|
||||
// see delegateWithCandidate function in [TxDelegate](TxDelegate)
|
||||
```
|
||||
|
||||
### TxEditCandidacy
|
||||
|
||||
If either the `Description` (excluding `DateBonded` which is constant),
|
||||
`Commission`, or the `GovernancePubKey` need to be updated, the
|
||||
`TxEditCandidacy` transaction should be sent from the owner account:
|
||||
|
||||
```golang
|
||||
type TxEditCandidacy struct {
|
||||
GovernancePubKey crypto.PubKey
|
||||
Commission int64
|
||||
Description Description
|
||||
}
|
||||
|
||||
editCandidacy(tx TxEditCandidacy):
|
||||
candidate = loadCandidate(store, tx.PubKey)
|
||||
if candidate == nil or candidate.Status == Revoked return
|
||||
|
||||
if tx.GovernancePubKey != nil candidate.GovernancePubKey = tx.GovernancePubKey
|
||||
if tx.Commission >= 0 candidate.Commission = tx.Commission
|
||||
if tx.Description != nil candidate.Description = tx.Description
|
||||
|
||||
saveCandidate(store, candidate)
|
||||
return
|
||||
```
|
||||
|
||||
### TxDelegate
|
||||
|
||||
Delegator bonds are created using the `TxDelegate` transaction. Within this
|
||||
transaction the delegator provides an amount of coins, and in return receives
|
||||
some amount of candidate's delegator shares that are assigned to
|
||||
`DelegatorBond.Shares`.
|
||||
|
||||
```golang
|
||||
type TxDelegate struct {
|
||||
PubKey crypto.PubKey
|
||||
Amount coin.Coin
|
||||
}
|
||||
|
||||
delegate(tx TxDelegate):
|
||||
candidate = loadCandidate(store, tx.PubKey)
|
||||
if candidate == nil return
|
||||
return delegateWithCandidate(tx, candidate)
|
||||
|
||||
delegateWithCandidate(tx TxDelegate, candidate Candidate):
|
||||
if candidate.Status == Revoked return
|
||||
|
||||
if candidate.Status == Bonded
|
||||
poolAccount = params.HoldBonded
|
||||
else
|
||||
poolAccount = params.HoldUnbonded
|
||||
|
||||
err = transfer(sender, poolAccount, tx.Amount)
|
||||
if err != nil return
|
||||
|
||||
bond = loadDelegatorBond(store, sender, tx.PubKey)
|
||||
if bond == nil then bond = DelegatorBond(tx.PubKey, rational.Zero, Coin(0), Coin(0))
|
||||
|
||||
issuedDelegatorShares = addTokens(tx.Amount, candidate)
|
||||
bond.Shares += issuedDelegatorShares
|
||||
|
||||
saveCandidate(store, candidate)
|
||||
saveDelegatorBond(store, sender, bond)
|
||||
saveGlobalState(store, gs)
|
||||
return
|
||||
|
||||
addTokens(amount coin.Coin, candidate Candidate):
|
||||
if candidate.Status == Bonded
|
||||
gs.BondedPool += amount
|
||||
issuedShares = amount / exchangeRate(gs.BondedShares, gs.BondedPool)
|
||||
gs.BondedShares += issuedShares
|
||||
else
|
||||
gs.UnbondedPool += amount
|
||||
issuedShares = amount / exchangeRate(gs.UnbondedShares, gs.UnbondedPool)
|
||||
gs.UnbondedShares += issuedShares
|
||||
|
||||
candidate.GlobalStakeShares += issuedShares
|
||||
|
||||
if candidate.IssuedDelegatorShares.IsZero()
|
||||
exRate = rational.One
|
||||
else
|
||||
exRate = candidate.GlobalStakeShares / candidate.IssuedDelegatorShares
|
||||
|
||||
issuedDelegatorShares = issuedShares / exRate
|
||||
candidate.IssuedDelegatorShares += issuedDelegatorShares
|
||||
return issuedDelegatorShares
|
||||
|
||||
exchangeRate(shares rational.Rat, tokenAmount int64):
|
||||
if shares.IsZero() then return rational.One
|
||||
return tokenAmount / shares
|
||||
|
||||
```
|
||||
|
||||
### TxUnbond
|
||||
|
||||
Delegator unbonding is defined with the following transaction:
|
||||
|
||||
```golang
|
||||
type TxUnbond struct {
|
||||
PubKey crypto.PubKey
|
||||
Shares rational.Rat
|
||||
}
|
||||
|
||||
unbond(tx TxUnbond):
|
||||
bond = loadDelegatorBond(store, sender, tx.PubKey)
|
||||
if bond == nil return
|
||||
if bond.Shares < tx.Shares return
|
||||
|
||||
bond.Shares -= tx.Shares
|
||||
|
||||
candidate = loadCandidate(store, tx.PubKey)
|
||||
|
||||
revokeCandidacy = false
|
||||
if bond.Shares.IsZero()
|
||||
if sender == candidate.Owner and candidate.Status != Revoked then revokeCandidacy = true then removeDelegatorBond(store, sender, bond)
|
||||
else
|
||||
saveDelegatorBond(store, sender, bond)
|
||||
|
||||
if candidate.Status == Bonded
|
||||
poolAccount = params.HoldBonded
|
||||
else
|
||||
poolAccount = params.HoldUnbonded
|
||||
|
||||
returnedCoins = removeShares(candidate, shares)
|
||||
|
||||
unbondDelegationElem = QueueElemUnbondDelegation(tx.PubKey, currentHeight(), sender, returnedCoins, startSlashRatio)
|
||||
unbondDelegationQueue.add(unbondDelegationElem)
|
||||
|
||||
transfer(poolAccount, unbondingPoolAddress, returnCoins)
|
||||
|
||||
if revokeCandidacy
|
||||
if candidate.Status == Bonded then bondedToUnbondedPool(candidate)
|
||||
candidate.Status = Revoked
|
||||
|
||||
if candidate.IssuedDelegatorShares.IsZero()
|
||||
removeCandidate(store, tx.PubKey)
|
||||
else
|
||||
saveCandidate(store, candidate)
|
||||
|
||||
saveGlobalState(store, gs)
|
||||
return
|
||||
|
||||
removeShares(candidate Candidate, shares rational.Rat):
|
||||
globalPoolSharesToRemove = delegatorShareExRate(candidate) * shares
|
||||
|
||||
if candidate.Status == Bonded
|
||||
gs.BondedShares -= globalPoolSharesToRemove
|
||||
removedTokens = exchangeRate(gs.BondedShares, gs.BondedPool) * globalPoolSharesToRemove
|
||||
gs.BondedPool -= removedTokens
|
||||
else
|
||||
gs.UnbondedShares -= globalPoolSharesToRemove
|
||||
removedTokens = exchangeRate(gs.UnbondedShares, gs.UnbondedPool) * globalPoolSharesToRemove
|
||||
gs.UnbondedPool -= removedTokens
|
||||
|
||||
candidate.GlobalStakeShares -= removedTokens
|
||||
candidate.IssuedDelegatorShares -= shares
|
||||
return returnedCoins
|
||||
|
||||
delegatorShareExRate(candidate Candidate):
|
||||
if candidate.IssuedDelegatorShares.IsZero() then return rational.One
|
||||
return candidate.GlobalStakeShares / candidate.IssuedDelegatorShares
|
||||
|
||||
bondedToUnbondedPool(candidate Candidate):
|
||||
removedTokens = exchangeRate(gs.BondedShares, gs.BondedPool) * candidate.GlobalStakeShares
|
||||
gs.BondedShares -= candidate.GlobalStakeShares
|
||||
gs.BondedPool -= removedTokens
|
||||
|
||||
gs.UnbondedPool += removedTokens
|
||||
issuedShares = removedTokens / exchangeRate(gs.UnbondedShares, gs.UnbondedPool)
|
||||
gs.UnbondedShares += issuedShares
|
||||
|
||||
candidate.GlobalStakeShares = issuedShares
|
||||
candidate.Status = Unbonded
|
||||
|
||||
return transfer(address of the bonded pool, address of the unbonded pool, removedTokens)
|
||||
```
|
||||
|
||||
### TxRedelegate
|
||||
|
||||
The re-delegation command allows delegators to switch validators while still
|
||||
receiving equal reward to as if they had never unbonded.
|
||||
|
||||
```golang
|
||||
type TxRedelegate struct {
|
||||
PubKeyFrom crypto.PubKey
|
||||
PubKeyTo crypto.PubKey
|
||||
Shares rational.Rat
|
||||
}
|
||||
|
||||
redelegate(tx TxRedelegate):
|
||||
bond = loadDelegatorBond(store, sender, tx.PubKey)
|
||||
if bond == nil then return
|
||||
|
||||
if bond.Shares < tx.Shares return
|
||||
candidate = loadCandidate(store, tx.PubKeyFrom)
|
||||
if candidate == nil return
|
||||
|
||||
candidate.RedelegatingShares += tx.Shares
|
||||
reDelegationElem = QueueElemReDelegate(tx.PubKeyFrom, currentHeight(), sender, tx.Shares, tx.PubKeyTo)
|
||||
redelegationQueue.add(reDelegationElem)
|
||||
return
|
||||
```
|
||||
|
||||
### TxLivelinessCheck
|
||||
|
||||
Liveliness issues are calculated by keeping track of the block precommits in
|
||||
the block header. A queue is persisted which contains the block headers from
|
||||
all recent blocks for the duration of the unbonding period. A validator is
|
||||
defined as having livliness issues if they have not been included in more than
|
||||
33% of the blocks over:
|
||||
* The most recent 24 Hours if they have >= 20% of global stake
|
||||
* The most recent week if they have = 0% of global stake
|
||||
* Linear interpolation of the above two scenarios
|
||||
|
||||
Liveliness kicks are only checked when a `TxLivelinessCheck` transaction is
|
||||
submitted.
|
||||
|
||||
```golang
|
||||
type TxLivelinessCheck struct {
|
||||
PubKey crypto.PubKey
|
||||
RewardAccount Addresss
|
||||
}
|
||||
```
|
||||
|
||||
If the `TxLivelinessCheck` is successful in kicking a validator, 5% of the
|
||||
liveliness punishment is provided as a reward to `RewardAccount`.
|
||||
|
||||
### TxProveLive
|
||||
|
||||
If the validator was kicked for liveliness issues and is able to regain
|
||||
liveliness then all delegators in the temporary unbonding pool which have not
|
||||
transacted to move will be bonded back to the now-live validator and begin to
|
||||
once again collect provisions and rewards. Regaining liveliness is demonstrated
|
||||
by sending in a `TxProveLive` transaction:
|
||||
|
||||
```golang
|
||||
type TxProveLive struct {
|
||||
PubKey crypto.PubKey
|
||||
}
|
||||
```
|
||||
|
||||
### End of block handling
|
||||
|
||||
```golang
|
||||
tick(ctx Context):
|
||||
hrsPerYr = 8766 // as defined by a julian year of 365.25 days
|
||||
|
||||
time = ctx.Time()
|
||||
if time > gs.InflationLastTime + ProvisionTimeout
|
||||
gs.InflationLastTime = time
|
||||
gs.Inflation = nextInflation(hrsPerYr).Round(1000000000)
|
||||
|
||||
provisions = gs.Inflation * (gs.TotalSupply / hrsPerYr)
|
||||
|
||||
gs.BondedPool += provisions
|
||||
gs.TotalSupply += provisions
|
||||
|
||||
saveGlobalState(store, gs)
|
||||
|
||||
if time > unbondDelegationQueue.head().InitTime + UnbondingPeriod
|
||||
for each element elem in the unbondDelegationQueue where time > elem.InitTime + UnbondingPeriod do
|
||||
transfer(unbondingQueueAddress, elem.Payout, elem.Tokens)
|
||||
unbondDelegationQueue.remove(elem)
|
||||
|
||||
if time > reDelegationQueue.head().InitTime + UnbondingPeriod
|
||||
for each element elem in the unbondDelegationQueue where time > elem.InitTime + UnbondingPeriod do
|
||||
candidate = getCandidate(store, elem.PubKey)
|
||||
returnedCoins = removeShares(candidate, elem.Shares)
|
||||
candidate.RedelegatingShares -= elem.Shares
|
||||
delegateWithCandidate(TxDelegate(elem.NewCandidate, returnedCoins), candidate)
|
||||
reDelegationQueue.remove(elem)
|
||||
|
||||
return UpdateValidatorSet()
|
||||
|
||||
nextInflation(hrsPerYr rational.Rat):
|
||||
if gs.TotalSupply > 0
|
||||
bondedRatio = gs.BondedPool / gs.TotalSupply
|
||||
else
|
||||
bondedRation = 0
|
||||
|
||||
inflationRateChangePerYear = (1 - bondedRatio / params.GoalBonded) * params.InflationRateChange
|
||||
inflationRateChange = inflationRateChangePerYear / hrsPerYr
|
||||
|
||||
inflation = gs.Inflation + inflationRateChange
|
||||
if inflation > params.InflationMax then inflation = params.InflationMax
|
||||
|
||||
if inflation < params.InflationMin then inflation = params.InflationMin
|
||||
|
||||
return inflation
|
||||
|
||||
UpdateValidatorSet():
|
||||
candidates = loadCandidates(store)
|
||||
|
||||
v1 = candidates.Validators()
|
||||
v2 = updateVotingPower(candidates).Validators()
|
||||
|
||||
change = v1.validatorsUpdated(v2) // determine all updated validators between two validator sets
|
||||
return change
|
||||
|
||||
updateVotingPower(candidates Candidates):
|
||||
foreach candidate in candidates do
|
||||
candidate.VotingPower = (candidate.IssuedDelegatorShares - candidate.RedelegatingShares) * delegatorShareExRate(candidate)
|
||||
|
||||
candidates.Sort()
|
||||
|
||||
foreach candidate in candidates do
|
||||
if candidate is not in the first params.MaxVals
|
||||
candidate.VotingPower = rational.Zero
|
||||
if candidate.Status == Bonded then bondedToUnbondedPool(candidate Candidate)
|
||||
|
||||
else if candidate.Status == UnBonded then unbondedToBondedPool(candidate)
|
||||
|
||||
saveCandidate(store, c)
|
||||
|
||||
return candidates
|
||||
|
||||
unbondedToBondedPool(candidate Candidate):
|
||||
removedTokens = exchangeRate(gs.UnbondedShares, gs.UnbondedPool) * candidate.GlobalStakeShares
|
||||
gs.UnbondedShares -= candidate.GlobalStakeShares
|
||||
gs.UnbondedPool -= removedTokens
|
||||
|
||||
gs.BondedPool += removedTokens
|
||||
issuedShares = removedTokens / exchangeRate(gs.BondedShares, gs.BondedPool)
|
||||
gs.BondedShares += issuedShares
|
||||
|
||||
candidate.GlobalStakeShares = issuedShares
|
||||
candidate.Status = Bonded
|
||||
|
||||
return transfer(address of the unbonded pool, address of the bonded pool, removedTokens)
|
||||
```
|
|
@ -0,0 +1,204 @@
|
|||
|
||||
## State
|
||||
|
||||
The staking module persists the following information to the store:
|
||||
* `GlobalState`, a struct describing the global pools, inflation, and
|
||||
fees
|
||||
* `ValidatorCandidates: <pubkey | shares> => <candidate>`, a map of all candidates (including current validators) in the store,
|
||||
indexed by their public key and shares in the global pool.
|
||||
* `DelegatorBonds: < delegator-address | candidate-pubkey > => <delegator-bond>`. a map of all delegations by a delegator to a candidate,
|
||||
indexed by delegator address and candidate pubkey.
|
||||
public key
|
||||
* `UnbondQueue`, the queue of unbonding delegations
|
||||
* `RedelegateQueue`, the queue of re-delegations
|
||||
|
||||
### Global State
|
||||
|
||||
The GlobalState contains information about the total amount of Atoms, the
|
||||
global bonded/unbonded position, the Atom inflation rate, and the fees.
|
||||
|
||||
`Params` is global data structure that stores system parameters and defines overall functioning of the
|
||||
module.
|
||||
|
||||
``` go
|
||||
type GlobalState struct {
|
||||
TotalSupply int64 // total supply of Atoms
|
||||
BondedPool int64 // reserve of bonded tokens
|
||||
BondedShares rational.Rat // sum of all shares distributed for the BondedPool
|
||||
UnbondedPool int64 // reserve of unbonding tokens held with candidates
|
||||
UnbondedShares rational.Rat // sum of all shares distributed for the UnbondedPool
|
||||
InflationLastTime int64 // timestamp of last processing of inflation
|
||||
Inflation rational.Rat // current annual inflation rate
|
||||
DateLastCommissionReset int64 // unix timestamp for last commission accounting reset
|
||||
FeePool coin.Coins // fee pool for all the fee shares which have already been distributed
|
||||
ReservePool coin.Coins // pool of reserve taxes collected on all fees for governance use
|
||||
Adjustment rational.Rat // Adjustment factor for calculating global fee accum
|
||||
}
|
||||
|
||||
type Params struct {
|
||||
HoldBonded Address // account where all bonded coins are held
|
||||
HoldUnbonding Address // account where all delegated but unbonding coins are held
|
||||
|
||||
InflationRateChange rational.Rational // maximum annual change in inflation rate
|
||||
InflationMax rational.Rational // maximum inflation rate
|
||||
InflationMin rational.Rational // minimum inflation rate
|
||||
GoalBonded rational.Rational // Goal of percent bonded atoms
|
||||
ReserveTax rational.Rational // Tax collected on all fees
|
||||
|
||||
MaxVals uint16 // maximum number of validators
|
||||
AllowedBondDenom string // bondable coin denomination
|
||||
|
||||
// gas costs for txs
|
||||
GasDeclareCandidacy int64
|
||||
GasEditCandidacy int64
|
||||
GasDelegate int64
|
||||
GasRedelegate int64
|
||||
GasUnbond int64
|
||||
}
|
||||
```
|
||||
|
||||
### Candidate
|
||||
|
||||
The `Candidate` holds the current state and some historical
|
||||
actions of validators or candidate-validators.
|
||||
|
||||
``` go
|
||||
type CandidateStatus byte
|
||||
|
||||
const (
|
||||
Bonded CandidateStatus = 0x01
|
||||
Unbonded CandidateStatus = 0x02
|
||||
Revoked CandidateStatus = 0x03
|
||||
)
|
||||
|
||||
type Candidate struct {
|
||||
Status CandidateStatus
|
||||
ConsensusPubKey crypto.PubKey
|
||||
GovernancePubKey crypto.PubKey
|
||||
Owner crypto.Address
|
||||
GlobalStakeShares rational.Rat
|
||||
IssuedDelegatorShares rational.Rat
|
||||
RedelegatingShares rational.Rat
|
||||
VotingPower rational.Rat
|
||||
Commission rational.Rat
|
||||
CommissionMax rational.Rat
|
||||
CommissionChangeRate rational.Rat
|
||||
CommissionChangeToday rational.Rat
|
||||
ProposerRewardPool coin.Coins
|
||||
Adjustment rational.Rat
|
||||
Description Description
|
||||
}
|
||||
|
||||
type Description struct {
|
||||
Name string
|
||||
DateBonded string
|
||||
Identity string
|
||||
Website string
|
||||
Details string
|
||||
}
|
||||
```
|
||||
|
||||
Candidate parameters are described:
|
||||
* Status: it can be Bonded (active validator), Unbonding (validator candidate)
|
||||
or Revoked
|
||||
* ConsensusPubKey: candidate public key that is used strictly for participating in
|
||||
consensus
|
||||
* GovernancePubKey: public key used by the validator for governance voting
|
||||
* Owner: Address that is allowed to unbond coins.
|
||||
* GlobalStakeShares: Represents shares of `GlobalState.BondedPool` if
|
||||
`Candidate.Status` is `Bonded`; or shares of `GlobalState.Unbondingt Pool`
|
||||
otherwise
|
||||
* IssuedDelegatorShares: Sum of all shares a candidate issued to delegators
|
||||
(which includes the candidate's self-bond); a delegator share represents
|
||||
their stake in the Candidate's `GlobalStakeShares`
|
||||
* RedelegatingShares: The portion of `IssuedDelegatorShares` which are
|
||||
currently re-delegating to a new validator
|
||||
* VotingPower: Proportional to the amount of bonded tokens which the validator
|
||||
has if `Candidate.Status` is `Bonded`; otherwise it is equal to `0`
|
||||
* Commission: The commission rate of fees charged to any delegators
|
||||
* CommissionMax: The maximum commission rate this candidate can charge each
|
||||
day from the date `GlobalState.DateLastCommissionReset`
|
||||
* CommissionChangeRate: The maximum daily increase of the candidate commission
|
||||
* CommissionChangeToday: Counter for the amount of change to commission rate
|
||||
which has occurred today, reset on the first block of each day (UTC time)
|
||||
* ProposerRewardPool: reward pool for extra fees collected when this candidate
|
||||
is the proposer of a block
|
||||
* Adjustment factor used to passively calculate each validators entitled fees
|
||||
from `GlobalState.FeePool`
|
||||
* Description
|
||||
* Name: moniker
|
||||
* DateBonded: date determined which the validator was bonded
|
||||
* Identity: optional field to provide a signature which verifies the
|
||||
validators identity (ex. UPort or Keybase)
|
||||
* Website: optional website link
|
||||
* Details: optional details
|
||||
|
||||
### DelegatorBond
|
||||
|
||||
Atom holders may delegate coins to candidates; under this circumstance their
|
||||
funds are held in a `DelegatorBond` data structure. It is owned by one
|
||||
delegator, and is associated with the shares for one candidate. The sender of
|
||||
the transaction is the owner of the bond.
|
||||
|
||||
``` go
|
||||
type DelegatorBond struct {
|
||||
Candidate crypto.PubKey
|
||||
Shares rational.Rat
|
||||
AdjustmentFeePool coin.Coins
|
||||
AdjustmentRewardPool coin.Coins
|
||||
}
|
||||
```
|
||||
|
||||
Description:
|
||||
* Candidate: the public key of the validator candidate: bonding too
|
||||
* Shares: the number of delegator shares received from the validator candidate
|
||||
* AdjustmentFeePool: Adjustment factor used to passively calculate each bonds
|
||||
entitled fees from `GlobalState.FeePool`
|
||||
* AdjustmentRewardPool: Adjustment factor used to passively calculate each
|
||||
bonds entitled fees from `Candidate.ProposerRewardPool`
|
||||
|
||||
|
||||
### QueueElem
|
||||
|
||||
The Unbonding and re-delegation process is implemented using the ordered queue
|
||||
data structure. All queue elements share a common structure:
|
||||
|
||||
```golang
|
||||
type QueueElem struct {
|
||||
Candidate crypto.PubKey
|
||||
InitTime int64 // when the element was added to the queue
|
||||
}
|
||||
```
|
||||
|
||||
The queue is ordered so the next element to unbond/re-delegate is at the head.
|
||||
Every tick the head of the queue is checked and if the unbonding period has
|
||||
passed since `InitTime`, the final settlement of the unbonding is started or
|
||||
re-delegation is executed, and the element is popped from the queue. Each
|
||||
`QueueElem` is persisted in the store until it is popped from the queue.
|
||||
|
||||
### QueueElemUnbondDelegation
|
||||
|
||||
QueueElemUnbondDelegation structure is used in the unbonding queue.
|
||||
|
||||
```golang
|
||||
type QueueElemUnbondDelegation struct {
|
||||
QueueElem
|
||||
Payout Address // account to pay out to
|
||||
Tokens coin.Coins // the value in Atoms of the amount of delegator shares which are unbonding
|
||||
StartSlashRatio rational.Rat // candidate slash ratio
|
||||
}
|
||||
```
|
||||
|
||||
### QueueElemReDelegate
|
||||
|
||||
QueueElemReDelegate structure is used in the re-delegation queue.
|
||||
|
||||
```golang
|
||||
type QueueElemReDelegate struct {
|
||||
QueueElem
|
||||
Payout Address // account to pay out to
|
||||
Shares rational.Rat // amount of shares which are unbonding
|
||||
NewCandidate crypto.PubKey // validator to bond to after unbond
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
|
||||
### Transaction Overview
|
||||
|
||||
Available Transactions:
|
||||
* TxDeclareCandidacy
|
||||
* TxEditCandidacy
|
||||
* TxDelegate
|
||||
* TxUnbond
|
||||
* TxRedelegate
|
||||
* TxProveLive
|
||||
|
||||
## Transaction processing
|
||||
|
||||
In this section we describe the processing of the transactions and the
|
||||
corresponding updates to the global state. In the following text we will use
|
||||
`gs` to refer to the `GlobalState` data structure, `unbondDelegationQueue` is a
|
||||
reference to the queue of unbond delegations, `reDelegationQueue` is the
|
||||
reference for the queue of redelegations. We use `tx` to denote a
|
||||
reference to a transaction that is being processed, and `sender` to denote the
|
||||
address of the sender of the transaction. We use function
|
||||
`loadCandidate(store, PubKey)` to obtain a Candidate structure from the store,
|
||||
and `saveCandidate(store, candidate)` to save it. Similarly, we use
|
||||
`loadDelegatorBond(store, sender, PubKey)` to load a delegator bond with the
|
||||
key (sender and PubKey) from the store, and
|
||||
`saveDelegatorBond(store, sender, bond)` to save it.
|
||||
`removeDelegatorBond(store, sender, bond)` is used to remove the bond from the
|
||||
store.
|
||||
|
||||
### TxDeclareCandidacy
|
||||
|
||||
A validator candidacy is declared using the `TxDeclareCandidacy` transaction.
|
||||
|
||||
```golang
|
||||
type TxDeclareCandidacy struct {
|
||||
ConsensusPubKey crypto.PubKey
|
||||
Amount coin.Coin
|
||||
GovernancePubKey crypto.PubKey
|
||||
Commission rational.Rat
|
||||
CommissionMax int64
|
||||
CommissionMaxChange int64
|
||||
Description Description
|
||||
}
|
||||
|
||||
declareCandidacy(tx TxDeclareCandidacy):
|
||||
candidate = loadCandidate(store, tx.PubKey)
|
||||
if candidate != nil return // candidate with that public key already exists
|
||||
|
||||
candidate = NewCandidate(tx.PubKey)
|
||||
candidate.Status = Unbonded
|
||||
candidate.Owner = sender
|
||||
init candidate VotingPower, GlobalStakeShares, IssuedDelegatorShares, RedelegatingShares and Adjustment to rational.Zero
|
||||
init commision related fields based on the values from tx
|
||||
candidate.ProposerRewardPool = Coin(0)
|
||||
candidate.Description = tx.Description
|
||||
|
||||
saveCandidate(store, candidate)
|
||||
|
||||
txDelegate = TxDelegate(tx.PubKey, tx.Amount)
|
||||
return delegateWithCandidate(txDelegate, candidate)
|
||||
|
||||
// see delegateWithCandidate function in [TxDelegate](TxDelegate)
|
||||
```
|
||||
|
||||
### TxEditCandidacy
|
||||
|
||||
If either the `Description` (excluding `DateBonded` which is constant),
|
||||
`Commission`, or the `GovernancePubKey` need to be updated, the
|
||||
`TxEditCandidacy` transaction should be sent from the owner account:
|
||||
|
||||
```golang
|
||||
type TxEditCandidacy struct {
|
||||
GovernancePubKey crypto.PubKey
|
||||
Commission int64
|
||||
Description Description
|
||||
}
|
||||
|
||||
editCandidacy(tx TxEditCandidacy):
|
||||
candidate = loadCandidate(store, tx.PubKey)
|
||||
if candidate == nil or candidate.Status == Revoked return
|
||||
|
||||
if tx.GovernancePubKey != nil candidate.GovernancePubKey = tx.GovernancePubKey
|
||||
if tx.Commission >= 0 candidate.Commission = tx.Commission
|
||||
if tx.Description != nil candidate.Description = tx.Description
|
||||
|
||||
saveCandidate(store, candidate)
|
||||
return
|
||||
```
|
||||
|
||||
### TxDelegate
|
||||
|
||||
Delegator bonds are created using the `TxDelegate` transaction. Within this
|
||||
transaction the delegator provides an amount of coins, and in return receives
|
||||
some amount of candidate's delegator shares that are assigned to
|
||||
`DelegatorBond.Shares`.
|
||||
|
||||
```golang
|
||||
type TxDelegate struct {
|
||||
PubKey crypto.PubKey
|
||||
Amount coin.Coin
|
||||
}
|
||||
|
||||
delegate(tx TxDelegate):
|
||||
candidate = loadCandidate(store, tx.PubKey)
|
||||
if candidate == nil return
|
||||
return delegateWithCandidate(tx, candidate)
|
||||
|
||||
delegateWithCandidate(tx TxDelegate, candidate Candidate):
|
||||
if candidate.Status == Revoked return
|
||||
|
||||
if candidate.Status == Bonded
|
||||
poolAccount = params.HoldBonded
|
||||
else
|
||||
poolAccount = params.HoldUnbonded
|
||||
|
||||
err = transfer(sender, poolAccount, tx.Amount)
|
||||
if err != nil return
|
||||
|
||||
bond = loadDelegatorBond(store, sender, tx.PubKey)
|
||||
if bond == nil then bond = DelegatorBond(tx.PubKey, rational.Zero, Coin(0), Coin(0))
|
||||
|
||||
issuedDelegatorShares = addTokens(tx.Amount, candidate)
|
||||
bond.Shares += issuedDelegatorShares
|
||||
|
||||
saveCandidate(store, candidate)
|
||||
saveDelegatorBond(store, sender, bond)
|
||||
saveGlobalState(store, gs)
|
||||
return
|
||||
|
||||
addTokens(amount coin.Coin, candidate Candidate):
|
||||
if candidate.Status == Bonded
|
||||
gs.BondedPool += amount
|
||||
issuedShares = amount / exchangeRate(gs.BondedShares, gs.BondedPool)
|
||||
gs.BondedShares += issuedShares
|
||||
else
|
||||
gs.UnbondedPool += amount
|
||||
issuedShares = amount / exchangeRate(gs.UnbondedShares, gs.UnbondedPool)
|
||||
gs.UnbondedShares += issuedShares
|
||||
|
||||
candidate.GlobalStakeShares += issuedShares
|
||||
|
||||
if candidate.IssuedDelegatorShares.IsZero()
|
||||
exRate = rational.One
|
||||
else
|
||||
exRate = candidate.GlobalStakeShares / candidate.IssuedDelegatorShares
|
||||
|
||||
issuedDelegatorShares = issuedShares / exRate
|
||||
candidate.IssuedDelegatorShares += issuedDelegatorShares
|
||||
return issuedDelegatorShares
|
||||
|
||||
exchangeRate(shares rational.Rat, tokenAmount int64):
|
||||
if shares.IsZero() then return rational.One
|
||||
return tokenAmount / shares
|
||||
|
||||
```
|
||||
|
||||
### TxUnbond
|
||||
|
||||
Delegator unbonding is defined with the following transaction:
|
||||
|
||||
```golang
|
||||
type TxUnbond struct {
|
||||
PubKey crypto.PubKey
|
||||
Shares rational.Rat
|
||||
}
|
||||
|
||||
unbond(tx TxUnbond):
|
||||
bond = loadDelegatorBond(store, sender, tx.PubKey)
|
||||
if bond == nil return
|
||||
if bond.Shares < tx.Shares return
|
||||
|
||||
bond.Shares -= tx.Shares
|
||||
|
||||
candidate = loadCandidate(store, tx.PubKey)
|
||||
|
||||
revokeCandidacy = false
|
||||
if bond.Shares.IsZero()
|
||||
if sender == candidate.Owner and candidate.Status != Revoked then revokeCandidacy = true then removeDelegatorBond(store, sender, bond)
|
||||
else
|
||||
saveDelegatorBond(store, sender, bond)
|
||||
|
||||
if candidate.Status == Bonded
|
||||
poolAccount = params.HoldBonded
|
||||
else
|
||||
poolAccount = params.HoldUnbonded
|
||||
|
||||
returnedCoins = removeShares(candidate, shares)
|
||||
|
||||
unbondDelegationElem = QueueElemUnbondDelegation(tx.PubKey, currentHeight(), sender, returnedCoins, startSlashRatio)
|
||||
unbondDelegationQueue.add(unbondDelegationElem)
|
||||
|
||||
transfer(poolAccount, unbondingPoolAddress, returnCoins)
|
||||
|
||||
if revokeCandidacy
|
||||
if candidate.Status == Bonded then bondedToUnbondedPool(candidate)
|
||||
candidate.Status = Revoked
|
||||
|
||||
if candidate.IssuedDelegatorShares.IsZero()
|
||||
removeCandidate(store, tx.PubKey)
|
||||
else
|
||||
saveCandidate(store, candidate)
|
||||
|
||||
saveGlobalState(store, gs)
|
||||
return
|
||||
|
||||
removeShares(candidate Candidate, shares rational.Rat):
|
||||
globalPoolSharesToRemove = delegatorShareExRate(candidate) * shares
|
||||
|
||||
if candidate.Status == Bonded
|
||||
gs.BondedShares -= globalPoolSharesToRemove
|
||||
removedTokens = exchangeRate(gs.BondedShares, gs.BondedPool) * globalPoolSharesToRemove
|
||||
gs.BondedPool -= removedTokens
|
||||
else
|
||||
gs.UnbondedShares -= globalPoolSharesToRemove
|
||||
removedTokens = exchangeRate(gs.UnbondedShares, gs.UnbondedPool) * globalPoolSharesToRemove
|
||||
gs.UnbondedPool -= removedTokens
|
||||
|
||||
candidate.GlobalStakeShares -= removedTokens
|
||||
candidate.IssuedDelegatorShares -= shares
|
||||
return returnedCoins
|
||||
|
||||
delegatorShareExRate(candidate Candidate):
|
||||
if candidate.IssuedDelegatorShares.IsZero() then return rational.One
|
||||
return candidate.GlobalStakeShares / candidate.IssuedDelegatorShares
|
||||
|
||||
bondedToUnbondedPool(candidate Candidate):
|
||||
removedTokens = exchangeRate(gs.BondedShares, gs.BondedPool) * candidate.GlobalStakeShares
|
||||
gs.BondedShares -= candidate.GlobalStakeShares
|
||||
gs.BondedPool -= removedTokens
|
||||
|
||||
gs.UnbondedPool += removedTokens
|
||||
issuedShares = removedTokens / exchangeRate(gs.UnbondedShares, gs.UnbondedPool)
|
||||
gs.UnbondedShares += issuedShares
|
||||
|
||||
candidate.GlobalStakeShares = issuedShares
|
||||
candidate.Status = Unbonded
|
||||
|
||||
return transfer(address of the bonded pool, address of the unbonded pool, removedTokens)
|
||||
```
|
||||
|
||||
### TxRedelegate
|
||||
|
||||
The re-delegation command allows delegators to switch validators while still
|
||||
receiving equal reward to as if they had never unbonded.
|
||||
|
||||
```golang
|
||||
type TxRedelegate struct {
|
||||
PubKeyFrom crypto.PubKey
|
||||
PubKeyTo crypto.PubKey
|
||||
Shares rational.Rat
|
||||
}
|
||||
|
||||
redelegate(tx TxRedelegate):
|
||||
bond = loadDelegatorBond(store, sender, tx.PubKey)
|
||||
if bond == nil then return
|
||||
|
||||
if bond.Shares < tx.Shares return
|
||||
candidate = loadCandidate(store, tx.PubKeyFrom)
|
||||
if candidate == nil return
|
||||
|
||||
candidate.RedelegatingShares += tx.Shares
|
||||
reDelegationElem = QueueElemReDelegate(tx.PubKeyFrom, currentHeight(), sender, tx.Shares, tx.PubKeyTo)
|
||||
redelegationQueue.add(reDelegationElem)
|
||||
return
|
||||
```
|
||||
|
||||
### TxProveLive
|
||||
|
||||
If a validator was automatically unbonded due to liveness issues and wishes to
|
||||
assert it is still online, it can send `TxProveLive`:
|
||||
|
||||
```golang
|
||||
type TxProveLive struct {
|
||||
PubKey crypto.PubKey
|
||||
}
|
||||
```
|
||||
|
||||
All delegators in the temporary unbonding pool which have not
|
||||
transacted to move will be bonded back to the now-live validator and begin to
|
||||
once again collect provisions and rewards.
|
||||
|
||||
```
|
||||
TODO: pseudo-code
|
||||
```
|
|
@ -0,0 +1,190 @@
|
|||
# Validator Set Changes
|
||||
|
||||
The validator set may be updated by state transitions that run at the beginning and
|
||||
end of every block. This can happen one of three ways:
|
||||
|
||||
- voting power of a validator changes due to bonding and unbonding
|
||||
- voting power of validator is "slashed" due to conflicting signed messages
|
||||
- validator is automatically unbonded due to inactivity
|
||||
|
||||
## Voting Power Changes
|
||||
|
||||
At the end of every block, we run the following:
|
||||
|
||||
(TODO remove inflation from here)
|
||||
|
||||
```golang
|
||||
tick(ctx Context):
|
||||
hrsPerYr = 8766 // as defined by a julian year of 365.25 days
|
||||
|
||||
time = ctx.Time()
|
||||
if time > gs.InflationLastTime + ProvisionTimeout
|
||||
gs.InflationLastTime = time
|
||||
gs.Inflation = nextInflation(hrsPerYr).Round(1000000000)
|
||||
|
||||
provisions = gs.Inflation * (gs.TotalSupply / hrsPerYr)
|
||||
|
||||
gs.BondedPool += provisions
|
||||
gs.TotalSupply += provisions
|
||||
|
||||
saveGlobalState(store, gs)
|
||||
|
||||
if time > unbondDelegationQueue.head().InitTime + UnbondingPeriod
|
||||
for each element elem in the unbondDelegationQueue where time > elem.InitTime + UnbondingPeriod do
|
||||
transfer(unbondingQueueAddress, elem.Payout, elem.Tokens)
|
||||
unbondDelegationQueue.remove(elem)
|
||||
|
||||
if time > reDelegationQueue.head().InitTime + UnbondingPeriod
|
||||
for each element elem in the unbondDelegationQueue where time > elem.InitTime + UnbondingPeriod do
|
||||
candidate = getCandidate(store, elem.PubKey)
|
||||
returnedCoins = removeShares(candidate, elem.Shares)
|
||||
candidate.RedelegatingShares -= elem.Shares
|
||||
delegateWithCandidate(TxDelegate(elem.NewCandidate, returnedCoins), candidate)
|
||||
reDelegationQueue.remove(elem)
|
||||
|
||||
return UpdateValidatorSet()
|
||||
|
||||
nextInflation(hrsPerYr rational.Rat):
|
||||
if gs.TotalSupply > 0
|
||||
bondedRatio = gs.BondedPool / gs.TotalSupply
|
||||
else
|
||||
bondedRation = 0
|
||||
|
||||
inflationRateChangePerYear = (1 - bondedRatio / params.GoalBonded) * params.InflationRateChange
|
||||
inflationRateChange = inflationRateChangePerYear / hrsPerYr
|
||||
|
||||
inflation = gs.Inflation + inflationRateChange
|
||||
if inflation > params.InflationMax then inflation = params.InflationMax
|
||||
|
||||
if inflation < params.InflationMin then inflation = params.InflationMin
|
||||
|
||||
return inflation
|
||||
|
||||
UpdateValidatorSet():
|
||||
candidates = loadCandidates(store)
|
||||
|
||||
v1 = candidates.Validators()
|
||||
v2 = updateVotingPower(candidates).Validators()
|
||||
|
||||
change = v1.validatorsUpdated(v2) // determine all updated validators between two validator sets
|
||||
return change
|
||||
|
||||
updateVotingPower(candidates Candidates):
|
||||
foreach candidate in candidates do
|
||||
candidate.VotingPower = (candidate.IssuedDelegatorShares - candidate.RedelegatingShares) * delegatorShareExRate(candidate)
|
||||
|
||||
candidates.Sort()
|
||||
|
||||
foreach candidate in candidates do
|
||||
if candidate is not in the first params.MaxVals
|
||||
candidate.VotingPower = rational.Zero
|
||||
if candidate.Status == Bonded then bondedToUnbondedPool(candidate Candidate)
|
||||
|
||||
else if candidate.Status == UnBonded then unbondedToBondedPool(candidate)
|
||||
|
||||
saveCandidate(store, c)
|
||||
|
||||
return candidates
|
||||
|
||||
unbondedToBondedPool(candidate Candidate):
|
||||
removedTokens = exchangeRate(gs.UnbondedShares, gs.UnbondedPool) * candidate.GlobalStakeShares
|
||||
gs.UnbondedShares -= candidate.GlobalStakeShares
|
||||
gs.UnbondedPool -= removedTokens
|
||||
|
||||
gs.BondedPool += removedTokens
|
||||
issuedShares = removedTokens / exchangeRate(gs.BondedShares, gs.BondedPool)
|
||||
gs.BondedShares += issuedShares
|
||||
|
||||
candidate.GlobalStakeShares = issuedShares
|
||||
candidate.Status = Bonded
|
||||
|
||||
return transfer(address of the unbonded pool, address of the bonded pool, removedTokens)
|
||||
```
|
||||
|
||||
|
||||
## Slashing
|
||||
|
||||
Messges which may compromise the safety of the underlying consensus protocol ("equivocations")
|
||||
result in some amount of the offending validator's shares being removed ("slashed").
|
||||
|
||||
Currently, such messages include only the following:
|
||||
|
||||
- prevotes by the same validator for more than one BlockID at the same
|
||||
Height and Round
|
||||
- precommits by the same validator for more than one BlockID at the same
|
||||
Height and Round
|
||||
|
||||
We call any such pair of conflicting votes `Evidence`. Full nodes in the network prioritize the
|
||||
detection and gossipping of `Evidence` so that it may be rapidly included in blocks and the offending
|
||||
validators punished.
|
||||
|
||||
For some `evidence` to be valid, it must satisfy:
|
||||
|
||||
`evidence.Timestamp >= block.Timestamp - MAX_EVIDENCE_AGE`
|
||||
|
||||
where `evidence.Timestamp` is the timestamp in the block at height
|
||||
`evidence.Height` and `block.Timestamp` is the current block timestamp.
|
||||
|
||||
If valid evidence is included in a block, the offending validator loses
|
||||
a constant `SLASH_PROPORTION` of their current stake at the beginning of the block:
|
||||
|
||||
```
|
||||
oldShares = validator.shares
|
||||
validator.shares = oldShares * (1 - SLASH_PROPORTION)
|
||||
```
|
||||
|
||||
This ensures that offending validators are punished the same amount whether they
|
||||
act as a single validator with X stake or as N validators with collectively X
|
||||
stake.
|
||||
|
||||
|
||||
## Automatic Unbonding
|
||||
|
||||
Every block includes a set of precommits by the validators for the previous block,
|
||||
known as the LastCommit. A LastCommit is valid so long as it contains precommits from +2/3 of voting power.
|
||||
|
||||
Proposers are incentivized to include precommits from all
|
||||
validators in the LastCommit by receiving additional fees
|
||||
proportional to the difference between the voting power included in the
|
||||
LastCommit and +2/3 (see [TODO](https://github.com/cosmos/cosmos-sdk/issues/967)).
|
||||
|
||||
Validators are penalized for failing to be included in the LastCommit for some
|
||||
number of blocks by being automatically unbonded.
|
||||
|
||||
The following information is stored with each validator candidate, and is only non-zero if the candidate becomes an active validator:
|
||||
|
||||
```go
|
||||
type ValidatorSigningInfo struct {
|
||||
StartHeight int64
|
||||
SignedBlocksBitArray BitArray
|
||||
}
|
||||
```
|
||||
|
||||
Where:
|
||||
* `StartHeight` is set to the height that the candidate became an active validator (with non-zero voting power).
|
||||
* `SignedBlocksBitArray` is a bit-array of size `SIGNED_BLOCKS_WINDOW` that records, for each of the last `SIGNED_BLOCKS_WINDOW` blocks,
|
||||
whether or not this validator was included in the LastCommit. It uses a `0` if the validator was included, and a `1` if it was not.
|
||||
Note it is initialized with all 0s.
|
||||
|
||||
At the beginning of each block, we update the signing info for each validator and check if they should be automatically unbonded:
|
||||
|
||||
```
|
||||
h = block.Height
|
||||
index = h % SIGNED_BLOCKS_WINDOW
|
||||
|
||||
for val in block.Validators:
|
||||
signInfo = val.SignInfo
|
||||
if val in block.LastCommit:
|
||||
signInfo.SignedBlocksBitArray.Set(index, 0)
|
||||
else
|
||||
signInfo.SignedBlocksBitArray.Set(index, 1)
|
||||
|
||||
// validator must be active for at least SIGNED_BLOCKS_WINDOW
|
||||
// before they can be automatically unbonded for failing to be
|
||||
// included in 50% of the recent LastCommits
|
||||
minHeight = signInfo.StartHeight + SIGNED_BLOCKS_WINDOW
|
||||
minSigned = SIGNED_BLOCKS_WINDOW / 2
|
||||
blocksSigned = signInfo.SignedBlocksBitArray.Sum()
|
||||
if h > minHeight AND blocksSigned < minSigned:
|
||||
unbond the validator
|
||||
```
|
|
@ -53,7 +53,7 @@ func handleMsgQuiz(ctx sdk.Context, k Keeper, msg MsgQuiz) sdk.Result {
|
|||
|
||||
bonusCoins := sdk.Coins{{msg.CoolAnswer, 69}}
|
||||
|
||||
_, err := k.ck.AddCoins(ctx, msg.Sender, bonusCoins)
|
||||
_, _, err := k.ck.AddCoins(ctx, msg.Sender, bonusCoins)
|
||||
if err != nil {
|
||||
return err.Result()
|
||||
}
|
||||
|
|
|
@ -125,7 +125,7 @@ func (k Keeper) CheckValid(ctx sdk.Context, difficulty uint64, count uint64) (ui
|
|||
|
||||
// Add some coins for a POW well done
|
||||
func (k Keeper) ApplyValid(ctx sdk.Context, sender sdk.Address, newDifficulty uint64, newCount uint64) sdk.Error {
|
||||
_, ckErr := k.ck.AddCoins(ctx, sender, []sdk.Coin{sdk.Coin{k.config.Denomination, k.config.Reward}})
|
||||
_, _, ckErr := k.ck.AddCoins(ctx, sender, []sdk.Coin{sdk.Coin{k.config.Denomination, k.config.Reward}})
|
||||
if ckErr != nil {
|
||||
return ckErr
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ func (k Keeper) Bond(ctx sdk.Context, addr sdk.Address, pubKey crypto.PubKey, st
|
|||
return 0, ErrIncorrectStakingToken(k.codespace)
|
||||
}
|
||||
|
||||
_, err := k.ck.SubtractCoins(ctx, addr, []sdk.Coin{stake})
|
||||
_, _, err := k.ck.SubtractCoins(ctx, addr, []sdk.Coin{stake})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ func (k Keeper) Unbond(ctx sdk.Context, addr sdk.Address) (crypto.PubKey, int64,
|
|||
|
||||
returnedBond := sdk.Coin{stakingToken, bi.Power}
|
||||
|
||||
_, err := k.ck.AddCoins(ctx, addr, []sdk.Coin{returnedBond})
|
||||
_, _, err := k.ck.AddCoins(ctx, addr, []sdk.Coin{returnedBond})
|
||||
if err != nil {
|
||||
return bi.PubKey, bi.Power, err
|
||||
}
|
||||
|
|
|
@ -176,7 +176,16 @@ func (st *iavlStore) Query(req abci.RequestQuery) (res abci.ResponseQuery) {
|
|||
} else {
|
||||
_, res.Value = tree.GetVersioned(key, height)
|
||||
}
|
||||
|
||||
case "/subspace":
|
||||
subspace := req.Data
|
||||
res.Key = subspace
|
||||
var KVs []KVPair
|
||||
iterator := st.SubspaceIterator(subspace)
|
||||
for ; iterator.Valid(); iterator.Next() {
|
||||
KVs = append(KVs, KVPair{iterator.Key(), iterator.Value()})
|
||||
}
|
||||
iterator.Close()
|
||||
res.Value = cdc.MustMarshalBinary(KVs)
|
||||
default:
|
||||
msg := fmt.Sprintf("Unexpected Query path: %v", req.Path)
|
||||
return sdk.ErrUnknownRequest(msg).QueryResult()
|
||||
|
|
|
@ -263,19 +263,40 @@ func TestIAVLStoreQuery(t *testing.T) {
|
|||
tree := iavl.NewVersionedTree(db, cacheSize)
|
||||
iavlStore := newIAVLStore(tree, numHistory)
|
||||
|
||||
k, v := []byte("wind"), []byte("blows")
|
||||
k2, v2 := []byte("water"), []byte("flows")
|
||||
v3 := []byte("is cold")
|
||||
// k3, v3 := []byte("earth"), []byte("soes")
|
||||
// k4, v4 := []byte("fire"), []byte("woes")
|
||||
k1, v1 := []byte("key1"), []byte("val1")
|
||||
k2, v2 := []byte("key2"), []byte("val2")
|
||||
v3 := []byte("val3")
|
||||
|
||||
ksub := []byte("key")
|
||||
KVs0 := []KVPair{}
|
||||
KVs1 := []KVPair{
|
||||
{k1, v1},
|
||||
{k2, v2},
|
||||
}
|
||||
KVs2 := []KVPair{
|
||||
{k1, v3},
|
||||
{k2, v2},
|
||||
}
|
||||
valExpSubEmpty := cdc.MustMarshalBinary(KVs0)
|
||||
valExpSub1 := cdc.MustMarshalBinary(KVs1)
|
||||
valExpSub2 := cdc.MustMarshalBinary(KVs2)
|
||||
|
||||
cid := iavlStore.Commit()
|
||||
ver := cid.Version
|
||||
query := abci.RequestQuery{Path: "/key", Data: k, Height: ver}
|
||||
query := abci.RequestQuery{Path: "/key", Data: k1, Height: ver}
|
||||
querySub := abci.RequestQuery{Path: "/subspace", Data: ksub, Height: ver}
|
||||
|
||||
// query subspace before anything set
|
||||
qres := iavlStore.Query(querySub)
|
||||
assert.Equal(t, uint32(sdk.CodeOK), qres.Code)
|
||||
assert.Equal(t, valExpSubEmpty, qres.Value)
|
||||
|
||||
// set data
|
||||
iavlStore.Set(k1, v1)
|
||||
iavlStore.Set(k2, v2)
|
||||
|
||||
// set data without commit, doesn't show up
|
||||
iavlStore.Set(k, v)
|
||||
qres := iavlStore.Query(query)
|
||||
qres = iavlStore.Query(query)
|
||||
assert.Equal(t, uint32(sdk.CodeOK), qres.Code)
|
||||
assert.Nil(t, qres.Value)
|
||||
|
||||
|
@ -289,17 +310,21 @@ func TestIAVLStoreQuery(t *testing.T) {
|
|||
query.Height = cid.Version
|
||||
qres = iavlStore.Query(query)
|
||||
assert.Equal(t, uint32(sdk.CodeOK), qres.Code)
|
||||
assert.Equal(t, v, qres.Value)
|
||||
assert.Equal(t, v1, qres.Value)
|
||||
|
||||
// and for the subspace
|
||||
qres = iavlStore.Query(querySub)
|
||||
assert.Equal(t, uint32(sdk.CodeOK), qres.Code)
|
||||
assert.Equal(t, valExpSub1, qres.Value)
|
||||
|
||||
// modify
|
||||
iavlStore.Set(k2, v2)
|
||||
iavlStore.Set(k, v3)
|
||||
iavlStore.Set(k1, v3)
|
||||
cid = iavlStore.Commit()
|
||||
|
||||
// query will return old values, as height is fixed
|
||||
qres = iavlStore.Query(query)
|
||||
assert.Equal(t, uint32(sdk.CodeOK), qres.Code)
|
||||
assert.Equal(t, v, qres.Value)
|
||||
assert.Equal(t, v1, qres.Value)
|
||||
|
||||
// update to latest in the query and we are happy
|
||||
query.Height = cid.Version
|
||||
|
@ -310,10 +335,14 @@ func TestIAVLStoreQuery(t *testing.T) {
|
|||
qres = iavlStore.Query(query2)
|
||||
assert.Equal(t, uint32(sdk.CodeOK), qres.Code)
|
||||
assert.Equal(t, v2, qres.Value)
|
||||
// and for the subspace
|
||||
qres = iavlStore.Query(querySub)
|
||||
assert.Equal(t, uint32(sdk.CodeOK), qres.Code)
|
||||
assert.Equal(t, valExpSub2, qres.Value)
|
||||
|
||||
// default (height 0) will show latest -1
|
||||
query0 := abci.RequestQuery{Path: "/store", Data: k}
|
||||
query0 := abci.RequestQuery{Path: "/store", Data: k1}
|
||||
qres = iavlStore.Query(query0)
|
||||
assert.Equal(t, uint32(sdk.CodeOK), qres.Code)
|
||||
assert.Equal(t, v, qres.Value)
|
||||
assert.Equal(t, v1, qres.Value)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ type MultiStore = types.MultiStore
|
|||
type CacheMultiStore = types.CacheMultiStore
|
||||
type CommitMultiStore = types.CommitMultiStore
|
||||
type KVStore = types.KVStore
|
||||
type KVPair = types.KVPair
|
||||
type Iterator = types.Iterator
|
||||
type CacheKVStore = types.CacheKVStore
|
||||
type CommitKVStore = types.CommitKVStore
|
||||
|
|
|
@ -2,7 +2,6 @@ package types
|
|||
|
||||
import (
|
||||
abci "github.com/tendermint/abci/types"
|
||||
cmn "github.com/tendermint/tmlibs/common"
|
||||
)
|
||||
|
||||
// Result is the union of ResponseDeliverTx and ResponseCheckTx.
|
||||
|
@ -31,7 +30,7 @@ type Result struct {
|
|||
ValidatorUpdates []abci.Validator
|
||||
|
||||
// Tags are used for transaction indexing and pubsub.
|
||||
Tags []cmn.KVPair
|
||||
Tags Tags
|
||||
}
|
||||
|
||||
// TODO: In the future, more codes may be OK.
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
abci "github.com/tendermint/abci/types"
|
||||
cmn "github.com/tendermint/tmlibs/common"
|
||||
dbm "github.com/tendermint/tmlibs/db"
|
||||
)
|
||||
|
||||
|
@ -256,3 +257,8 @@ func PrefixEndBytes(prefix []byte) []byte {
|
|||
}
|
||||
return end
|
||||
}
|
||||
|
||||
//----------------------------------------
|
||||
|
||||
// key-value result for iterator queries
|
||||
type KVPair cmn.KVPair
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
cmn "github.com/tendermint/tmlibs/common"
|
||||
)
|
||||
|
||||
// Type synonym for convenience
|
||||
type Tag = cmn.KVPair
|
||||
|
||||
// Type synonym for convenience
|
||||
type Tags cmn.KVPairs
|
||||
|
||||
// New empty tags
|
||||
func EmptyTags() Tags {
|
||||
return make(Tags, 0)
|
||||
}
|
||||
|
||||
// Append a single tag
|
||||
func (t Tags) AppendTag(k string, v []byte) Tags {
|
||||
return append(t, MakeTag(k, v))
|
||||
}
|
||||
|
||||
// Append two lists of tags
|
||||
func (t Tags) AppendTags(a Tags) Tags {
|
||||
return append(t, a...)
|
||||
}
|
||||
|
||||
// New variadic tags, must be k string, v []byte repeating
|
||||
func NewTags(tags ...interface{}) Tags {
|
||||
var ret Tags
|
||||
if len(tags)%2 != 0 {
|
||||
panic("must specify key-value pairs as varargs")
|
||||
}
|
||||
i := 0
|
||||
for {
|
||||
if i == len(tags) {
|
||||
break
|
||||
}
|
||||
ret = append(ret, Tag{Key: []byte(tags[i].(string)), Value: tags[i+1].([]byte)})
|
||||
i += 2
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Make a tag from a key and a value
|
||||
func MakeTag(k string, v []byte) Tag {
|
||||
return Tag{Key: []byte(k), Value: v}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAppendTags(t *testing.T) {
|
||||
a := NewTags("a", []byte("1"))
|
||||
b := NewTags("b", []byte("2"))
|
||||
c := a.AppendTags(b)
|
||||
require.Equal(t, c, Tags{MakeTag("a", []byte("1")), MakeTag("b", []byte("2"))})
|
||||
}
|
||||
|
||||
func TestEmptyTags(t *testing.T) {
|
||||
a := EmptyTags()
|
||||
require.Equal(t, a, Tags{})
|
||||
}
|
||||
|
||||
func TestNewTags(t *testing.T) {
|
||||
b := NewTags("a", []byte("1"))
|
||||
require.Equal(t, b, Tags{MakeTag("a", []byte("1"))})
|
||||
}
|
|
@ -25,13 +25,14 @@ func NewHandler(k Keeper) sdk.Handler {
|
|||
func handleMsgSend(ctx sdk.Context, k Keeper, msg MsgSend) sdk.Result {
|
||||
// NOTE: totalIn == totalOut should already have been checked
|
||||
|
||||
err := k.InputOutputCoins(ctx, msg.Inputs, msg.Outputs)
|
||||
tags, err := k.InputOutputCoins(ctx, msg.Inputs, msg.Outputs)
|
||||
if err != nil {
|
||||
return err.Result()
|
||||
}
|
||||
|
||||
// TODO: add some tags so we can search it!
|
||||
return sdk.Result{} // TODO
|
||||
return sdk.Result{
|
||||
Tags: tags,
|
||||
}
|
||||
}
|
||||
|
||||
// Handle MsgIssue.
|
||||
|
|
|
@ -32,22 +32,22 @@ func (keeper Keeper) HasCoins(ctx sdk.Context, addr sdk.Address, amt sdk.Coins)
|
|||
}
|
||||
|
||||
// SubtractCoins subtracts amt from the coins at the addr.
|
||||
func (keeper Keeper) SubtractCoins(ctx sdk.Context, addr sdk.Address, amt sdk.Coins) (sdk.Coins, sdk.Error) {
|
||||
func (keeper Keeper) SubtractCoins(ctx sdk.Context, addr sdk.Address, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) {
|
||||
return subtractCoins(ctx, keeper.am, addr, amt)
|
||||
}
|
||||
|
||||
// AddCoins adds amt to the coins at the addr.
|
||||
func (keeper Keeper) AddCoins(ctx sdk.Context, addr sdk.Address, amt sdk.Coins) (sdk.Coins, sdk.Error) {
|
||||
func (keeper Keeper) AddCoins(ctx sdk.Context, addr sdk.Address, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) {
|
||||
return addCoins(ctx, keeper.am, addr, amt)
|
||||
}
|
||||
|
||||
// SendCoins moves coins from one account to another
|
||||
func (keeper Keeper) SendCoins(ctx sdk.Context, fromAddr sdk.Address, toAddr sdk.Address, amt sdk.Coins) sdk.Error {
|
||||
func (keeper Keeper) SendCoins(ctx sdk.Context, fromAddr sdk.Address, toAddr sdk.Address, amt sdk.Coins) (sdk.Tags, sdk.Error) {
|
||||
return sendCoins(ctx, keeper.am, fromAddr, toAddr, amt)
|
||||
}
|
||||
|
||||
// InputOutputCoins handles a list of inputs and outputs
|
||||
func (keeper Keeper) InputOutputCoins(ctx sdk.Context, inputs []Input, outputs []Output) sdk.Error {
|
||||
func (keeper Keeper) InputOutputCoins(ctx sdk.Context, inputs []Input, outputs []Output) (sdk.Tags, sdk.Error) {
|
||||
return inputOutputCoins(ctx, keeper.am, inputs, outputs)
|
||||
}
|
||||
|
||||
|
@ -74,12 +74,12 @@ func (keeper SendKeeper) HasCoins(ctx sdk.Context, addr sdk.Address, amt sdk.Coi
|
|||
}
|
||||
|
||||
// SendCoins moves coins from one account to another
|
||||
func (keeper SendKeeper) SendCoins(ctx sdk.Context, fromAddr sdk.Address, toAddr sdk.Address, amt sdk.Coins) sdk.Error {
|
||||
func (keeper SendKeeper) SendCoins(ctx sdk.Context, fromAddr sdk.Address, toAddr sdk.Address, amt sdk.Coins) (sdk.Tags, sdk.Error) {
|
||||
return sendCoins(ctx, keeper.am, fromAddr, toAddr, amt)
|
||||
}
|
||||
|
||||
// InputOutputCoins handles a list of inputs and outputs
|
||||
func (keeper SendKeeper) InputOutputCoins(ctx sdk.Context, inputs []Input, outputs []Output) sdk.Error {
|
||||
func (keeper SendKeeper) InputOutputCoins(ctx sdk.Context, inputs []Input, outputs []Output) (sdk.Tags, sdk.Error) {
|
||||
return inputOutputCoins(ctx, keeper.am, inputs, outputs)
|
||||
}
|
||||
|
||||
|
@ -131,59 +131,65 @@ func hasCoins(ctx sdk.Context, am sdk.AccountMapper, addr sdk.Address, amt sdk.C
|
|||
}
|
||||
|
||||
// SubtractCoins subtracts amt from the coins at the addr.
|
||||
func subtractCoins(ctx sdk.Context, am sdk.AccountMapper, addr sdk.Address, amt sdk.Coins) (sdk.Coins, sdk.Error) {
|
||||
func subtractCoins(ctx sdk.Context, am sdk.AccountMapper, addr sdk.Address, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) {
|
||||
oldCoins := getCoins(ctx, am, addr)
|
||||
newCoins := oldCoins.Minus(amt)
|
||||
if !newCoins.IsNotNegative() {
|
||||
return amt, sdk.ErrInsufficientCoins(fmt.Sprintf("%s < %s", oldCoins, amt))
|
||||
return amt, nil, sdk.ErrInsufficientCoins(fmt.Sprintf("%s < %s", oldCoins, amt))
|
||||
}
|
||||
err := setCoins(ctx, am, addr, newCoins)
|
||||
return newCoins, err
|
||||
tags := sdk.NewTags("sender", addr.Bytes())
|
||||
return newCoins, tags, err
|
||||
}
|
||||
|
||||
// AddCoins adds amt to the coins at the addr.
|
||||
func addCoins(ctx sdk.Context, am sdk.AccountMapper, addr sdk.Address, amt sdk.Coins) (sdk.Coins, sdk.Error) {
|
||||
func addCoins(ctx sdk.Context, am sdk.AccountMapper, addr sdk.Address, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) {
|
||||
oldCoins := getCoins(ctx, am, addr)
|
||||
newCoins := oldCoins.Plus(amt)
|
||||
if !newCoins.IsNotNegative() {
|
||||
return amt, sdk.ErrInsufficientCoins(fmt.Sprintf("%s < %s", oldCoins, amt))
|
||||
return amt, nil, sdk.ErrInsufficientCoins(fmt.Sprintf("%s < %s", oldCoins, amt))
|
||||
}
|
||||
err := setCoins(ctx, am, addr, newCoins)
|
||||
return newCoins, err
|
||||
tags := sdk.NewTags("recipient", addr.Bytes())
|
||||
return newCoins, tags, err
|
||||
}
|
||||
|
||||
// SendCoins moves coins from one account to another
|
||||
// NOTE: Make sure to revert state changes from tx on error
|
||||
func sendCoins(ctx sdk.Context, am sdk.AccountMapper, fromAddr sdk.Address, toAddr sdk.Address, amt sdk.Coins) sdk.Error {
|
||||
_, err := subtractCoins(ctx, am, fromAddr, amt)
|
||||
func sendCoins(ctx sdk.Context, am sdk.AccountMapper, fromAddr sdk.Address, toAddr sdk.Address, amt sdk.Coins) (sdk.Tags, sdk.Error) {
|
||||
_, subTags, err := subtractCoins(ctx, am, fromAddr, amt)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = addCoins(ctx, am, toAddr, amt)
|
||||
_, addTags, err := addCoins(ctx, am, toAddr, amt)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil
|
||||
return subTags.AppendTags(addTags), nil
|
||||
}
|
||||
|
||||
// InputOutputCoins handles a list of inputs and outputs
|
||||
// NOTE: Make sure to revert state changes from tx on error
|
||||
func inputOutputCoins(ctx sdk.Context, am sdk.AccountMapper, inputs []Input, outputs []Output) sdk.Error {
|
||||
func inputOutputCoins(ctx sdk.Context, am sdk.AccountMapper, inputs []Input, outputs []Output) (sdk.Tags, sdk.Error) {
|
||||
allTags := sdk.EmptyTags()
|
||||
|
||||
for _, in := range inputs {
|
||||
_, err := subtractCoins(ctx, am, in.Address, in.Coins)
|
||||
_, tags, err := subtractCoins(ctx, am, in.Address, in.Coins)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
allTags = allTags.AppendTags(tags)
|
||||
}
|
||||
|
||||
for _, out := range outputs {
|
||||
_, err := addCoins(ctx, am, out.Address, out.Coins)
|
||||
_, tags, err := addCoins(ctx, am, out.Address, out.Coins)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
allTags = allTags.AppendTags(tags)
|
||||
}
|
||||
|
||||
return nil
|
||||
return allTags, nil
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ func TestKeeper(t *testing.T) {
|
|||
coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 5}})
|
||||
assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 15}}))
|
||||
|
||||
_, err := coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 11}})
|
||||
_, _, err := coinKeeper.SubtractCoins(ctx, addr, sdk.Coins{{"barcoin", 11}})
|
||||
assert.Implements(t, (*sdk.Error)(nil), err)
|
||||
assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"barcoin", 10}, {"foocoin", 15}}))
|
||||
|
||||
|
@ -78,7 +78,7 @@ func TestKeeper(t *testing.T) {
|
|||
assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}}))
|
||||
assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"foocoin", 5}}))
|
||||
|
||||
err2 := coinKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"foocoin", 50}})
|
||||
_, err2 := coinKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"foocoin", 50}})
|
||||
assert.Implements(t, (*sdk.Error)(nil), err2)
|
||||
assert.True(t, coinKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}}))
|
||||
assert.True(t, coinKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"foocoin", 5}}))
|
||||
|
@ -147,7 +147,7 @@ func TestSendKeeper(t *testing.T) {
|
|||
assert.True(t, sendKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}}))
|
||||
assert.True(t, sendKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"foocoin", 5}}))
|
||||
|
||||
err2 := sendKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"foocoin", 50}})
|
||||
_, err2 := sendKeeper.SendCoins(ctx, addr, addr2, sdk.Coins{{"foocoin", 50}})
|
||||
assert.Implements(t, (*sdk.Error)(nil), err2)
|
||||
assert.True(t, sendKeeper.GetCoins(ctx, addr).IsEqual(sdk.Coins{{"foocoin", 10}}))
|
||||
assert.True(t, sendKeeper.GetCoins(ctx, addr2).IsEqual(sdk.Coins{{"foocoin", 5}}))
|
||||
|
|
|
@ -25,7 +25,7 @@ func NewHandler(ibcm Mapper, ck bank.Keeper) sdk.Handler {
|
|||
func handleIBCTransferMsg(ctx sdk.Context, ibcm Mapper, ck bank.Keeper, msg IBCTransferMsg) sdk.Result {
|
||||
packet := msg.IBCPacket
|
||||
|
||||
_, err := ck.SubtractCoins(ctx, packet.SrcAddr, packet.Coins)
|
||||
_, _, err := ck.SubtractCoins(ctx, packet.SrcAddr, packet.Coins)
|
||||
if err != nil {
|
||||
return err.Result()
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ func handleIBCReceiveMsg(ctx sdk.Context, ibcm Mapper, ck bank.Keeper, msg IBCRe
|
|||
return ErrInvalidSequence(ibcm.codespace).Result()
|
||||
}
|
||||
|
||||
_, err := ck.AddCoins(ctx, packet.DestAddr, packet.Coins)
|
||||
_, _, err := ck.AddCoins(ctx, packet.DestAddr, packet.Coins)
|
||||
if err != nil {
|
||||
return err.Result()
|
||||
}
|
||||
|
|
|
@ -34,7 +34,8 @@ func newAddress() crypto.Address {
|
|||
|
||||
func getCoins(ck bank.Keeper, ctx sdk.Context, addr crypto.Address) (sdk.Coins, sdk.Error) {
|
||||
zero := sdk.Coins(nil)
|
||||
return ck.AddCoins(ctx, addr, zero)
|
||||
coins, _, err := ck.AddCoins(ctx, addr, zero)
|
||||
return coins, err
|
||||
}
|
||||
|
||||
func makeCodec() *wire.Codec {
|
||||
|
@ -70,7 +71,7 @@ func TestIBC(t *testing.T) {
|
|||
zero := sdk.Coins(nil)
|
||||
mycoins := sdk.Coins{sdk.Coin{"mycoin", 10}}
|
||||
|
||||
coins, err := ck.AddCoins(ctx, src, mycoins)
|
||||
coins, _, err := ck.AddCoins(ctx, src, mycoins)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, mycoins, coins)
|
||||
|
||||
|
|
|
@ -15,42 +15,6 @@ import (
|
|||
"github.com/cosmos/cosmos-sdk/x/stake"
|
||||
)
|
||||
|
||||
//// create command to query for all candidates
|
||||
//func GetCmdQueryCandidates(storeName string, cdc *wire.Codec) *cobra.Command {
|
||||
//cmd := &cobra.Command{
|
||||
//Use: "candidates",
|
||||
//Short: "Query for the set of validator-candidates pubkeys",
|
||||
//RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
//key := stake.CandidatesKey
|
||||
|
||||
//ctx := context.NewCoreContextFromViper()
|
||||
//res, err := ctx.Query(key, storeName)
|
||||
//if err != nil {
|
||||
//return err
|
||||
//}
|
||||
|
||||
//// parse out the candidates
|
||||
//candidates := new(stake.Candidates)
|
||||
//err = cdc.UnmarshalBinary(res, candidates)
|
||||
//if err != nil {
|
||||
//return err
|
||||
//}
|
||||
//output, err := wire.MarshalJSONIndent(cdc, candidates)
|
||||
//if err != nil {
|
||||
//return err
|
||||
//}
|
||||
//fmt.Println(string(output))
|
||||
//return nil
|
||||
|
||||
//// TODO output with proofs / machine parseable etc.
|
||||
//},
|
||||
//}
|
||||
|
||||
//cmd.Flags().AddFlagSet(fsDelegator)
|
||||
//return cmd
|
||||
//}
|
||||
|
||||
// get the command to query a candidate
|
||||
func GetCmdQueryCandidate(storeName string, cdc *wire.Codec) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
|
@ -64,9 +28,7 @@ func GetCmdQueryCandidate(storeName string, cdc *wire.Codec) *cobra.Command {
|
|||
}
|
||||
|
||||
key := stake.GetCandidateKey(addr)
|
||||
|
||||
ctx := context.NewCoreContextFromViper()
|
||||
|
||||
res, err := ctx.Query(key, storeName)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -74,10 +36,7 @@ func GetCmdQueryCandidate(storeName string, cdc *wire.Codec) *cobra.Command {
|
|||
|
||||
// parse out the candidate
|
||||
candidate := new(stake.Candidate)
|
||||
err = cdc.UnmarshalBinary(res, candidate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cdc.MustUnmarshalBinary(res, candidate)
|
||||
output, err := wire.MarshalJSONIndent(cdc, candidate)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -93,6 +52,41 @@ func GetCmdQueryCandidate(storeName string, cdc *wire.Codec) *cobra.Command {
|
|||
return cmd
|
||||
}
|
||||
|
||||
// get the command to query a candidate
|
||||
func GetCmdQueryCandidates(storeName string, cdc *wire.Codec) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "candidates",
|
||||
Short: "Query for all validator-candidate accounts",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
key := stake.CandidatesKey
|
||||
ctx := context.NewCoreContextFromViper()
|
||||
resKVs, err := ctx.QuerySubspace(cdc, key, storeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// parse out the candidates
|
||||
var candidates []stake.Candidate
|
||||
for _, KV := range resKVs {
|
||||
var candidate stake.Candidate
|
||||
cdc.MustUnmarshalBinary(KV.Value, &candidate)
|
||||
candidates = append(candidates, candidate)
|
||||
}
|
||||
|
||||
output, err := wire.MarshalJSONIndent(cdc, candidates)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(output))
|
||||
return nil
|
||||
|
||||
// TODO output with proofs / machine parseable etc.
|
||||
},
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// get the command to query a single delegator bond
|
||||
func GetCmdQueryDelegatorBond(storeName string, cdc *wire.Codec) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
|
@ -112,9 +106,7 @@ func GetCmdQueryDelegatorBond(storeName string, cdc *wire.Codec) *cobra.Command
|
|||
delegator := crypto.Address(bz)
|
||||
|
||||
key := stake.GetDelegatorBondKey(delegator, addr, cdc)
|
||||
|
||||
ctx := context.NewCoreContextFromViper()
|
||||
|
||||
res, err := ctx.Query(key, storeName)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -122,10 +114,7 @@ func GetCmdQueryDelegatorBond(storeName string, cdc *wire.Codec) *cobra.Command
|
|||
|
||||
// parse out the bond
|
||||
bond := new(stake.DelegatorBond)
|
||||
err = cdc.UnmarshalBinary(res, bond)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cdc.MustUnmarshalBinary(res, bond)
|
||||
output, err := wire.MarshalJSONIndent(cdc, bond)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -142,44 +131,42 @@ func GetCmdQueryDelegatorBond(storeName string, cdc *wire.Codec) *cobra.Command
|
|||
return cmd
|
||||
}
|
||||
|
||||
//// get the command to query all the candidates bonded to a delegator
|
||||
//func GetCmdQueryDelegatorBonds(storeName string, cdc *wire.Codec) *cobra.Command {
|
||||
//cmd := &cobra.Command{
|
||||
//Use: "delegator-candidates",
|
||||
//Short: "Query all delegators bond's candidate-addresses based on delegator-address",
|
||||
//RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// get the command to query all the candidates bonded to a delegator
|
||||
func GetCmdQueryDelegatorBonds(storeName string, cdc *wire.Codec) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delegator-candidates",
|
||||
Short: "Query all delegators bonds based on delegator-address",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
|
||||
//bz, err := hex.DecodeString(viper.GetString(FlagAddressDelegator))
|
||||
//if err != nil {
|
||||
//return err
|
||||
//}
|
||||
//delegator := crypto.Address(bz)
|
||||
delegatorAddr, err := sdk.GetAddress(viper.GetString(FlagAddressDelegator))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key := stake.GetDelegatorBondsKey(delegatorAddr, cdc)
|
||||
ctx := context.NewCoreContextFromViper()
|
||||
resKVs, err := ctx.QuerySubspace(cdc, key, storeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//key := stake.GetDelegatorBondsKey(delegator, cdc)
|
||||
// parse out the candidates
|
||||
var delegators []stake.DelegatorBond
|
||||
for _, KV := range resKVs {
|
||||
var delegator stake.DelegatorBond
|
||||
cdc.MustUnmarshalBinary(KV.Value, &delegator)
|
||||
delegators = append(delegators, delegator)
|
||||
}
|
||||
|
||||
//ctx := context.NewCoreContextFromViper()
|
||||
output, err := wire.MarshalJSONIndent(cdc, delegators)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(output))
|
||||
return nil
|
||||
|
||||
//res, err := ctx.Query(key, storeName)
|
||||
//if err != nil {
|
||||
//return err
|
||||
//}
|
||||
|
||||
//// parse out the candidates list
|
||||
//var candidates []crypto.PubKey
|
||||
//err = cdc.UnmarshalBinary(res, candidates)
|
||||
//if err != nil {
|
||||
//return err
|
||||
//}
|
||||
//output, err := wire.MarshalJSONIndent(cdc, candidates)
|
||||
//if err != nil {
|
||||
//return err
|
||||
//}
|
||||
//fmt.Println(string(output))
|
||||
//return nil
|
||||
|
||||
//// TODO output with proofs / machine parseable etc.
|
||||
//},
|
||||
//}
|
||||
//cmd.Flags().AddFlagSet(fsDelegator)
|
||||
//return cmd
|
||||
//}
|
||||
// TODO output with proofs / machine parseable etc.
|
||||
},
|
||||
}
|
||||
cmd.Flags().AddFlagSet(fsDelegator)
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ func GetCmdDeclareCandidacy(cdc *wire.Codec) *cobra.Command {
|
|||
Use: "declare-candidacy",
|
||||
Short: "create new validator-candidate account and delegate some coins to it",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc))
|
||||
|
||||
amount, err := sdk.ParseCoin(viper.GetString(FlagAmount))
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -56,8 +58,6 @@ func GetCmdDeclareCandidacy(cdc *wire.Codec) *cobra.Command {
|
|||
msg := stake.NewMsgDeclareCandidacy(candidateAddr, pk, amount, description)
|
||||
|
||||
// build and sign the transaction, then broadcast to Tendermint
|
||||
ctx := context.NewCoreContextFromViper().WithDecoder(authcmd.GetAccountDecoder(cdc))
|
||||
|
||||
res, err := ctx.EnsureSignBuildBroadcast(ctx.FromAddressName, msg, cdc)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -97,14 +97,18 @@ func handleMsgDeclareCandidacy(ctx sdk.Context, msg MsgDeclareCandidacy, k Keepe
|
|||
|
||||
candidate := NewCandidate(msg.CandidateAddr, msg.PubKey, msg.Description)
|
||||
k.setCandidate(ctx, candidate)
|
||||
tags := sdk.NewTags("action", []byte("declareCandidacy"), "candidate", msg.CandidateAddr.Bytes(), "moniker", []byte(msg.Description.Moniker), "identity", []byte(msg.Description.Identity))
|
||||
|
||||
// move coins from the msg.Address account to a (self-bond) delegator account
|
||||
// the candidate account and global shares are updated within here
|
||||
err := delegate(ctx, k, msg.CandidateAddr, msg.Bond, candidate)
|
||||
delegateTags, err := delegate(ctx, k, msg.CandidateAddr, msg.Bond, candidate)
|
||||
if err != nil {
|
||||
return err.Result()
|
||||
}
|
||||
return sdk.Result{}
|
||||
tags = tags.AppendTags(delegateTags)
|
||||
return sdk.Result{
|
||||
Tags: tags,
|
||||
}
|
||||
}
|
||||
|
||||
func handleMsgEditCandidacy(ctx sdk.Context, msg MsgEditCandidacy, k Keeper) sdk.Result {
|
||||
|
@ -128,7 +132,10 @@ func handleMsgEditCandidacy(ctx sdk.Context, msg MsgEditCandidacy, k Keeper) sdk
|
|||
candidate.Description.Details = msg.Description.Details
|
||||
|
||||
k.setCandidate(ctx, candidate)
|
||||
return sdk.Result{}
|
||||
tags := sdk.NewTags("action", []byte("editCandidacy"), "candidate", msg.CandidateAddr.Bytes(), "moniker", []byte(msg.Description.Moniker), "identity", []byte(msg.Description.Identity))
|
||||
return sdk.Result{
|
||||
Tags: tags,
|
||||
}
|
||||
}
|
||||
|
||||
func handleMsgDelegate(ctx sdk.Context, msg MsgDelegate, k Keeper) sdk.Result {
|
||||
|
@ -148,16 +155,18 @@ func handleMsgDelegate(ctx sdk.Context, msg MsgDelegate, k Keeper) sdk.Result {
|
|||
GasUsed: GasDelegate,
|
||||
}
|
||||
}
|
||||
err := delegate(ctx, k, msg.DelegatorAddr, msg.Bond, candidate)
|
||||
tags, err := delegate(ctx, k, msg.DelegatorAddr, msg.Bond, candidate)
|
||||
if err != nil {
|
||||
return err.Result()
|
||||
}
|
||||
return sdk.Result{}
|
||||
return sdk.Result{
|
||||
Tags: tags,
|
||||
}
|
||||
}
|
||||
|
||||
// common functionality between handlers
|
||||
func delegate(ctx sdk.Context, k Keeper, delegatorAddr sdk.Address,
|
||||
bondAmt sdk.Coin, candidate Candidate) sdk.Error {
|
||||
bondAmt sdk.Coin, candidate Candidate) (sdk.Tags, sdk.Error) {
|
||||
|
||||
// Get or create the delegator bond
|
||||
bond, found := k.GetDelegatorBond(ctx, delegatorAddr, candidate.Address)
|
||||
|
@ -171,9 +180,9 @@ func delegate(ctx sdk.Context, k Keeper, delegatorAddr sdk.Address,
|
|||
|
||||
// Account new shares, save
|
||||
pool := k.GetPool(ctx)
|
||||
_, err := k.coinKeeper.SubtractCoins(ctx, bond.DelegatorAddr, sdk.Coins{bondAmt})
|
||||
_, _, err := k.coinKeeper.SubtractCoins(ctx, bond.DelegatorAddr, sdk.Coins{bondAmt})
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
pool, candidate, newShares := pool.candidateAddTokens(candidate, bondAmt.Amount)
|
||||
bond.Shares = bond.Shares.Add(newShares)
|
||||
|
@ -184,7 +193,8 @@ func delegate(ctx sdk.Context, k Keeper, delegatorAddr sdk.Address,
|
|||
k.setDelegatorBond(ctx, bond)
|
||||
k.setCandidate(ctx, candidate)
|
||||
k.setPool(ctx, pool)
|
||||
return nil
|
||||
tags := sdk.NewTags("action", []byte("delegate"), "delegator", delegatorAddr.Bytes(), "candidate", candidate.Address.Bytes())
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func handleMsgUnbond(ctx sdk.Context, msg MsgUnbond, k Keeper) sdk.Result {
|
||||
|
@ -281,5 +291,8 @@ func handleMsgUnbond(ctx sdk.Context, msg MsgUnbond, k Keeper) sdk.Result {
|
|||
k.setCandidate(ctx, candidate)
|
||||
}
|
||||
k.setPool(ctx, p)
|
||||
return sdk.Result{}
|
||||
tags := sdk.NewTags("action", []byte("unbond"), "delegator", msg.DelegatorAddr.Bytes(), "candidate", msg.CandidateAddr.Bytes())
|
||||
return sdk.Result{
|
||||
Tags: tags,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"encoding/json"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/wire"
|
||||
crypto "github.com/tendermint/go-crypto"
|
||||
)
|
||||
|
||||
|
@ -19,6 +20,12 @@ const StakingToken = "steak"
|
|||
//Verify interface at compile time
|
||||
var _, _, _, _ sdk.Msg = &MsgDeclareCandidacy{}, &MsgEditCandidacy{}, &MsgDelegate{}, &MsgUnbond{}
|
||||
|
||||
var msgCdc = wire.NewCodec()
|
||||
|
||||
func init() {
|
||||
wire.RegisterCrypto(msgCdc)
|
||||
}
|
||||
|
||||
//______________________________________________________________________
|
||||
|
||||
// MsgDeclareCandidacy - struct for unbonding transactions
|
||||
|
@ -45,11 +52,7 @@ func (msg MsgDeclareCandidacy) GetSigners() []sdk.Address { return []sdk.Address
|
|||
|
||||
// get the bytes for the message signer to sign on
|
||||
func (msg MsgDeclareCandidacy) GetSignBytes() []byte {
|
||||
b, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
return msgCdc.MustMarshalBinary(msg)
|
||||
}
|
||||
|
||||
// quick validity check
|
||||
|
|
Loading…
Reference in New Issue