diff --git a/docs/spec/README.md b/docs/spec/README.md new file mode 100644 index 000000000000..6a507dc03191 --- /dev/null +++ b/docs/spec/README.md @@ -0,0 +1,13 @@ +# Cosmos Hub Spec + +This directory contains specifications for the application level components of the Cosmos Hub. + +NOTE: the specifications are not yet complete and very much a work in progress. + +- [Basecoin](basecoin) - Cosmos SDK related specifications and transactions for sending tokens. +- [Staking](staking) - Proof of Stake related specifications including bonding and delegation transactions, inflation, fees, etc. +- [Governance](governance) - Governance related specifications including proposals and voting. +- [Other](other) - Other components of the Cosmos Hub, including the reserve pool, All in Bits vesting, etc. + +The [specification for Tendermint](https://github.com/tendermint/tendermint/tree/develop/docs/specification/new-spec), +i.e. the underlying blockchain, can be found elsewhere. diff --git a/docs/spec/basecoin/basecoin.md b/docs/spec/basecoin/basecoin.md new file mode 100644 index 000000000000..8c2c47fd4e02 --- /dev/null +++ b/docs/spec/basecoin/basecoin.md @@ -0,0 +1,2 @@ +- SDK related specifications (ie. how multistore, signatures, etc. work). +- Basecoin (SendTx) diff --git a/docs/spec/governance/governance.md b/docs/spec/governance/governance.md new file mode 100644 index 000000000000..c8560cee6f54 --- /dev/null +++ b/docs/spec/governance/governance.md @@ -0,0 +1,590 @@ +# 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:** Validator start signaling that they are ready to switch to the new version + - **Switch:** Once more than 2/3rd validators have signaled their readiness 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 `TxProposal` 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 such that `0 < deposit < 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 `mMxDepositPeriod` (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 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. + +### 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` 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 ratio of `Yes` votes to `No` 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 is the ratio of `Yes` votes to `No` votes at the end of the voting period is superior to 50% and if the number of `NoWithVeto` votes is inferior to 1/3rd of total votes (excluding `Abstain`). + +`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 validaotor, it will override its validator vote with its own vote. 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 if the vote is closed (i.e. `MinDeposit` was reached and `Voting period` is over), then this 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 an additional 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 PubKey`. From there, they will be able to sign vote 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* + +### 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 int64 // Minimum value of Yes votes to No votes ratio 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 + + ProcedureNumber int16 // Incremented each time a new procedure is created + IsActive bool // If true, procedure is active. Only one procedure can have isActive true. +} +``` + +### Proposals + +`Proposals` are item to be voted on. They 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. +} + +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. + Votes map[string]int64 // Votes for each option (Yes, No, NoWithVeto, Abstain) +} +``` + +Each `Proposal` is identified by its unique `proposalID`. + +Additionaly, four lists will be linked to each proposal: +- `DepositorList`: List of addresses that deposited on the proposal with their associated deposit +- `VotersList`: List of addresses that voted **under each validator** with their associated option +- `InitVotingPowerList`: Snapshot of validators' voting power **when proposal enters voting period** (only saves validators whose voting power is >0). +- `MinusesList`: List of minuses for each validator. Used to compute validators' voting power when they cast a vote. + +Two final parameters, `InitTotalVotingPower` and `InitProcedureNumber` associated with `proposalID` will be saved when proposal enters voting period. + +We also introduce `ProposalProcessingQueue` which lists all the `ProposalIDs` of proposals that reached `MinDeposit` from oldest to newest. 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 new first 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`. + +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 + // check if proposal is correctly formatted. Includes fee payment. + + if !correctlyFormatted(txGovSubmitProposal) then + 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 -= InitialDeposit + + proposalID = generate new proposalID + proposal = create new Proposal from proposalID + + proposal.Title = txGovSubmitProposal.Title + proposal.Description = txGovSubmitProposal.Description + proposal.Type = txGovSubmitProposal.Type + proposal.Category = txGovSubmitProposal.Category + proposal.Deposit = txGovSubmitProposal.InitialDeposit + proposal.SubmitBlock = CurrentBlock + + create depositorsList from proposalID + initiate deposit of sender in depositorsList at txGovSubmitProposal.InitialDeposit + + if (txGovSubmitProposal.InitialDeposit < ActiveProcedure.MinDeposit) then + // MinDeposit is not reached + + proposal.VotingStartBlock = -1 + + else + // MinDeposit is reached + + proposal.VotingStartBlock = CurrentBlock + + create votersList, + initVotingPowerList, + minusesList, + initProcedureNumber, + initTotalVotingPower from proposalID + + snapshot(ActiveProcedure.ProcedureNumber) // Save current procedure number in initProcedureNumber + snapshot(TotalVotingPower) // Save total voting power in initTotalVotingPower + snapshot(ValidatorVotingPower) // Save validators' voting power in initVotingPowerList + + ProposalProcessingQueueEnd++ + ProposalProcessingQueue[ProposalProcessingQueueEnd] = proposalID + + return proposalID +``` + +And the pseudocode for the `ProposalProcessingQueue`: + +``` + in BeginBlock do + + checkProposal() + + + + func checkProposal() + if (ProposalProcessingQueueBeginning == ProposalProcessingQueueEnd) + return + + else + retrieve proposalID from ProposalProcessingQueue[ProposalProcessingQueueBeginning] + retrieve proposal from proposalID + retrieve initProcedureNumber from proposalID + retrieve initProcedure from initProcedureNumber + + if (CurrentBlock == proposal.VotingStartBlock + initProcedure.VotingPeriod) + retrieve initVotingPowerList from proposalID + retrieve votersList from proposalID + retrieve validators from initVotingPowerList + + for each validator in validators + if validator is not in votersList + slash validator by ActiveProcedure.GovernancePenalty + + ProposalProcessingQueueBeginning++ // ProposalProcessingQueue will have a new element + checkProposal() + + else + return +``` + +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 +} +``` + +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 + if !exist(txGovDeposit.proposalID) 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 + retrieve proposal from txGovDeposit.ProposalID // retrieve throws if it fails + + 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 + + retrieve depositorsList from txGovDeposit.ProposalID + sender.AtomBalance -= txGovDeposit.Deposit + + if sender is in depositorsList + increase deposit of sender in depositorsList by txGovDeposit.Deposit + + else + initialise deposit of sender in depositorsList at txGovDeposit.Deposit + + proposal.Deposit += txGovDeposit.Deposit + + if (proposal.Deposit >= ActiveProcedure.MinDeposit) then + // MinDeposit is reached, vote opens + + proposal.VotingStartBlock = CurrentBlock + + create votersList, + initVotingPowerList, + minusesList, + initProcedureNumber, + initTotalVotingPower from proposalID + + snapshot(ActiveProcedure.ProcedureNumber) // Save current procedure number in InitProcedureNumber + snapshot(TotalVotingPower) // Save total voting power in InitTotalVotingPower + snapshot(ValidatorVotingPower) // Save validators' voting power in InitVotingPowerList + + ProposalProcessingQueueEnd++ // ProposalProcessingQueue will have a new element + ProposalProcessingQueue[ProposalProcessingQueueEnd] = txGovDeposit.ProposalID +``` + +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 + } +``` + +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 + if !exists(txGovClaimDeposit.ProposalID) then + // There is no proposal for this proposalID + + throw + + else + retrieve depositorsList from txGovClaimDeposit.ProposalID + + + if sender is not in depositorsList then + throw + + else + retrieve deposit from sender in depositorsList + + if deposit <= 0 + // deposit has already been claimed + + throw + + else + retrieve proposal from txGovClaimDeposit.ProposalID + + if proposal.VotingStartBlock <= 0 + // Vote never started + + if (CurrentBlock <= proposal.SubmitBlock + ActiveProcedure.MaxDepositPeriod) + // MaxDepositPeriod is not reached + + throw + + else + // MaxDepositPeriod is reached + + set deposit of sender in depositorsList to 0 + sender.AtomBalance += deposit + + else + // Vote started + + retrieve initTotalVotingPower from txGovClaimDeposit.ProposalID + retrieve initProcedureNumber from txGovClaimDeposit.ProposalID + retrieve initProcedure from initProcedureNumber // get procedure that was active when vote opened + + if (proposal.Category AND proposal.Votes['Yes']/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 + + set deposit of sender in depositorsList to 0 + sender.AtomBalance += deposit + + else + throw +``` + + +### 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 + } +``` + +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 + if !exists(txGovVote.proposalID) OR + + // Throws if + // proposalID does not exist + + throw + + else + retrieve initProcedureNumber from txGovVote.ProposalID + retrieve initProcedure from initProcedureNumber // get procedure that was active when vote opened + + if !initProcedure.OptionSet.includes(txGovVote.Option) OR + !isValid(txGovVote.ValidatorPubKey) 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 + retrieve votersList from txGovVote.ProposalID + + if sender is in votersList under txGovVote.ValidatorPubKey then + // sender has already voted with the Atoms bonded to ValidatorPubKey + + throw + + else + retrieve proposal from txGovVote.ProposalID + retrieve InitTotalVotingPower from txGovVote.ProposalID + + 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']/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 + // sender can vote, check if sender == validator and add sender to voter list + + add sender to votersList under txGovVote.ValidatorPubKey + + if (sender is not equal to GovPubKey that corresponds to txGovVote.ValidatorPubKey) + // 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 + throw + + else + if txGovVote.ValidatorPubKey is not in votersList under txGovVote.ValidatorPubKey then + // Validator has not voted already + + if exists(MinusesList[txGovVote.ValidatorPubKey]) then + // a minus already exists for this validator's PubKey, increase minus + // by the amount of Atoms sender has bonded to ValidatorPubKey + + MinusesList[txGovVote.ValidatorPubKey] += sender.bondedAmountTo(txGovVote.ValidatorPubKey) + + else + // a minus does not already exist for this validator's PubKey, initialise minus + // at the amount of Atoms sender has bonded to ValidatorPubKey + + MinusesList[txGovVote.ValidatorPubKey] = sender.bondedAmountTo(txGovVote.ValidatorPubKey) + + else + // Validator has already voted + // Reduce option count chosen by validator by sender's bonded Amount + + retrieve validatorOption from votersList using txGovVote.ValidatorPubKey + proposal.Votes['validatorOption'] -= sender.bondedAmountTo(txGovVote.ValidatorPubKey) + + // increase Option count 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 + + retrieve initialVotingPower from InitVotingPowerList using txGovVote.ValidatorPubKey + + + if exists(MinusesList[txGovVote.ValidatorPubKey]) then + // a minus exists for this validator's PubKey, decrease vote of validator by minus + + proposal.Votes['txGovVote.Option'] += (initialVotingPower - MinusesList[txGovVote.ValidatorPubKey]) + + else + // a minus does not exist for this validator's PubKey, validator votes with full voting power + + proposal.Votes['txGovVote.Option'] += initialVotingPower + + if (proposal.Category AND proposal.Votes['Yes']/InitTotalVotingPower >= 2/3) + // after vote is counted, if proposal is urgent and special condition is met + // remove proposalID from ProposalProcessingQueue + + remove txGovVote.ProposalID from ProposalProcessingQueue + Rearrange ProposalProcessingQueue + ProposalProcessingQueueEnd-- +``` + + +## 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. diff --git a/docs/spec/other/other.md b/docs/spec/other/other.md new file mode 100644 index 000000000000..0ba8e612caed --- /dev/null +++ b/docs/spec/other/other.md @@ -0,0 +1,2 @@ +- reserve pool +- AiB vesting diff --git a/docs/spec/staking/AbsoluteFeeDistrModel.xlsx b/docs/spec/staking/AbsoluteFeeDistrModel.xlsx new file mode 100644 index 000000000000..a252fa749d78 Binary files /dev/null and b/docs/spec/staking/AbsoluteFeeDistrModel.xlsx differ diff --git a/docs/spec/staking/definitions and examples.md b/docs/spec/staking/definitions and examples.md new file mode 100644 index 000000000000..72dbc3a3d4e9 --- /dev/null +++ b/docs/spec/staking/definitions and examples.md @@ -0,0 +1,115 @@ +# Staking Module + +## Basic Terms and Definitions + +- Cosmsos Hub - a Tendermint-based Proof of Stake blockchain system +- Atom - native token of the Cosmsos Hub +- Atom holder - an entity that holds some amount of Atoms +- Candidate - an Atom holder that is actively involved in Tendermint blockchain protocol (running Tendermint Full Node +TODO: add link to Full Node definition) and is competing with other candidates to be elected as a Validator +(TODO: add link to Validator definition)) +- Validator - a candidate that is currently selected among a set of candidates to be able to sign protocol messages +in the Tendermint consensus protocol +- Delegator - an Atom holder that has bonded any of its Atoms by delegating them to a validator (or a candidate) +- Bonding Atoms - a process of locking Atoms in a bond deposit (putting Atoms under protocol control). +Atoms are always bonded through a validator (or candidate) process. Bonded atoms can be slashed (burned) in case a +validator process misbehaves (does not behave according to a protocol specification). Atom holder can regain access +to its bonded Atoms (if they are not slashed in the meantime), i.e., they can be moved to its account, +after Unbonding period has expired. +- Unbonding period - a period of time after which Atom holder gains access to its bonded Atoms (they can be withdrawn +to a user account) or they can re-delegate +- Inflationary provisions - inflation is a process of increasing Atom supply. Atoms are being created in the process of +(Cosmos Hub) blocks creation. Owners of bonded atoms are rewarded for securing network with inflationary provisions +proportional to it's bonded Atom share. +- Transaction fees - transaction fee is a fee that is included in the Cosmsos Hub transactions. The fees are collected +by the current validator set and distributed among validators and delegators in proportion to it's bonded Atom share. +- Commission fee - a fee taken from the transaction fees by a validator for it's service + +## The pool and the share + +At the core of the Staking module is the concept of a pool which denotes collection of Atoms contributed by different +Atom holders. There are two global pools in the Staking module: bonded pool and unbonded pool. Bonded Atoms are part +of the global bonded pool. On the other side, if a candidate or delegator wants to unbond its Atoms, those Atoms are +kept in the unbonding pool for a duration of the unbonding period. In the Staking module, a pool is logical concept, +i.e., there is no pool data structure that would be responsible for managing pool resources. Instead, it is managed +in a distributed way. More precisely, at the global level, for each pool, we track only the total amount of +(bonded or unbonded) Atoms and a current amount of issued shares. A share is a unit of Atom distribution and the +value of the share (share-to-atom exchange rate) is changing during the system execution. The +share-to-atom exchange rate can be computed as: + +`share-to-atom-ex-rate = size of the pool / ammount of issued shares` + +Then for each candidate (in a per candidate data structure) we keep track of an amount of shares the candidate is owning +in a pool. At any point in time, the exact amount of Atoms a candidate has in the pool +can be computed as the number of shares it owns multiplied with the share-to-atom exchange rate: + +`candidate-coins = candidate.Shares * share-to-atom-ex-rate` + +The benefit of such accounting of the pool resources is the fact that a modification to the pool because of +bonding/unbonding/slashing/provisioning of atoms affects only global data (size of the pool and the number of shares) +and the related validator/candidate data structure, i.e., the data structure of other validators do not need to be +modified. Let's explain this further with several small examples: + +We consider initially 4 validators p1, p2, p3 and p4, and that each validator has bonded 10 Atoms +to a bonded pool. Furthermore, let's assume that we have issued initially 40 shares (note that the initial distribution +of the shares, i.e., share-to-atom exchange rate can be set to any meaningful value), i.e., +share-to-atom-ex-rate = 1 atom per share. Then at the global pool level we have, the size of the pool is 40 Atoms, and +the amount of issued shares is equal to 40. And for each validator we store in their corresponding data structure +that each has 10 shares of the bonded pool. Now lets assume that the validator p4 starts process of unbonding of 5 +shares. Then the total size of the pool is decreased and now it will be 35 shares and the amount of Atoms is 35. +Note that the only change in other data structures needed is reducing the number of shares for a validator p4 from 10 +to 5. + +Let's consider now the case where a validator p1 wants to bond 15 more atoms to the pool. Now the size of the pool +is 50, and as the exchange rate hasn't changed (1 share is still worth 1 Atom), we need to create more shares, +i.e. we now have 50 shares in the pool in total. +Validators p2, p3 and p4 still have (correspondingly) 10, 10 and 5 shares each worth of 1 atom per share, so we +don't need to modify anything in their corresponding data structures. But p1 now has 25 shares, so we update the amount +of shares owned by the p1 in its data structure. Note that apart from the size of the pool that is in Atoms, all other +data structures refer only to shares. + +Finally, let's consider what happens when new Atoms are created and added to the pool due to inflation. Let's assume that +the inflation rate is 10 percent and that it is applied to the current state of the pool. This means that 5 Atoms are +created and added to the pool and that each validator now proportionally increase it's Atom count. Let's analyse how this +change is reflected in the data structures. First, the size of the pool is increased and is now 55 atoms. As a share of +each validator in the pool hasn't changed, this means that the total number of shares stay the same (50) and that the +amount of shares of each validator stays the same (correspondingly 25, 10, 10, 5). But the exchange rate has changed and +each share is now worth 55/50 Atoms per share, so each validator has effectively increased amount of Atoms it has. +So validators now have (correspondingly) 55/2, 55/5, 55/5 and 55/10 Atoms. + +The concepts of the pool and its shares is at the core of the accounting in the Staking module. It is used for managing +the global pools (such as bonding and unbonding pool), but also for distribution of Atoms between validator and its +delegators (we will explain this in section X). + +#### Delegator shares + +A candidate is, depending on it's status, contributing Atoms to either the bonded or unbonded pool, and in return gets +some amount of (global) pool shares. Note that not all those Atoms (and respective shares) are owned by the candidate +as some Atoms could be delegated to a candidate. The mechanism for distribution of Atoms (and shares) between a +candidate and it's delegators is based on a notion of delegator shares. More precisely, every candidate is issuing +(local) delegator shares (`Candidate.IssuedDelegatorShares`) that represents some portion of global shares +managed by the candidate (`Candidate.GlobalStakeShares`). The principle behind managing delegator shares is the same +as described in [Section](#The pool and the share). We now illustrate it with an example. + +Lets consider 4 validators p1, p2, p3 and p4, and assume that each validator has bonded 10 Atoms to a bonded pool. +Furthermore, lets assume that we have issued initially 40 global shares, i.e., that `share-to-atom-ex-rate = 1 atom per +share`. So we will `GlobalState.BondedPool = 40` and `GlobalState.BondedShares = 40` and in the Candidate data structure +of each validator `Candidate.GlobalStakeShares = 10`. Furthermore, each validator issued 10 delegator +shares which are initially owned by itself, i.e., `Candidate.IssuedDelegatorShares = 10`, where +`delegator-share-to-global-share-ex-rate = 1 global share per delegator share`. +Now lets assume that a delegator d1 delegates 5 atoms to a validator p1 and consider what are the updates we need +to make to the data structures. First, `GlobalState.BondedPool = 45` and `GlobalState.BondedShares = 45`. Then, +for validator p1 we have `Candidate.GlobalStakeShares = 15`, but we also need to issue also additional delegator shares, +i.e., `Candidate.IssuedDelegatorShares = 15` as the delegator d1 now owns 5 delegator shares of validator p1, where +each delegator share is worth 1 global shares, i.e, 1 Atom. Lets see now what happens after 5 new Atoms are created due +to inflation. In that case, we only need to update `GlobalState.BondedPool` which is now equal to 50 Atoms as created +Atoms are added to the bonded pool. Note that the amount of global and delegator shares stay the same but they are now +worth more as share-to-atom-ex-rate is now worth 50/45 Atoms per share. Therefore, a delegator d1 now owns + +`delegatorCoins = 10 (delegator shares) * 1 (delegator-share-to-global-share-ex-rate) * 50/45 (share-to-atom-ex-rate) = 100/9 Atoms` + + + + + + diff --git a/docs/spec/staking/old/spec.md b/docs/spec/staking/old/spec.md new file mode 100644 index 000000000000..bd87ec028586 --- /dev/null +++ b/docs/spec/staking/old/spec.md @@ -0,0 +1,675 @@ +# Stake Module + +## Overview + +The stake module is tasked with various core staking functionality. Through the +stake module atoms may be bonded, delegated, and provisions/rewards are +distributed. Atom provisions are distributed to validators and their delegators +through share distribution of a collective pool of all staked atoms. As atoms +are created they are added to the common pool and each share become +proportionally worth more atoms. Fees are distributed through a similar pooling +mechanism but where each validator and delegator maintains an adjustment factor +to determine the true proportion of fees they are entitled too. This adjustment +factor is updated for each delegator and validator for each block where changes +to the voting power occurs in the network. Broken down, the stake module at a +high level is responsible for: + - Declaration of candidacy for becoming a validator + - Updating Tendermint validating power to reflect slashable stake + - Delegation and unbonding transactions + - Implementing unbonding period + - Provisioning Atoms + - Managing and distributing transaction fees + - Providing the framework for validator commission on delegators + +### Transaction Overview + +Available Transactions: + - TxDeclareCandidacy + - TxEditCandidacy + - TxLivelinessCheck + - TxProveLive + - TxDelegate + - TxUnbond + - TxRedelegate + +## Global State + +`Params` and `GlobalState` represent the global persistent state of Gaia. +`Params` is intended to remain static whereas `GlobalState` is anticipated to +change each block. + +``` golang +type Params struct { + HoldBonded Address // account where all bonded coins are held + HoldUnbonded Address // account where all delegated but unbonded 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 +} +``` + +``` golang +type GlobalState struct { + TotalSupply int64 // total supply of atom tokens + BondedShares rational.Rat // sum of all shares distributed for the BondedPool + UnbondedShares rational.Rat // sum of all shares distributed for the UnbondedPool + BondedPool int64 // reserve of bonded tokens + UnbondedPool int64 // reserve of unbonded tokens held with candidates + 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 +} +``` + +### The Queue + +The queue is ordered so the next 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 `InitHeight` commence with final settlement of the unbonding and pop the +queue. All queue elements used for unbonding share a common struct: + +``` golang +type QueueElem struct { + Candidate crypto.PubKey + InitHeight int64 // when the queue was initiated +} +``` + +Each `QueueElem` is persisted in the store until it is popped from the queue. + +## Validator-Candidate + +The `Candidate` struct holds the current state and some historical actions of +validators or candidate-validators. + +``` golang +type Candidate struct { + Status CandidateStatus + PubKey crypto.PubKey + GovernancePubKey crypto.PubKey + Owner 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 CandidateStatus byte +const ( + VyingUnbonded CandidateStatus = 0x00 + VyingUnbonding CandidateStatus = 0x01 + Bonded CandidateStatus = 0x02 + KickUnbonding CandidateStatus = 0x03 + KickUnbonded CandidateStatus = 0x04 +) + +type Description struct { + Name string + DateBonded string + Identity string + Website string + Details string +} +``` + +Candidate parameters are described: + - Status: signal that the candidate is either vying for validator status + either unbonded or unbonding, an active validator, or a kicked validator + either unbonding or unbonded. + - PubKey: separated key from the owner of the candidate as is used strictly + for participating in consensus. + - Owner: Address where coins are bonded from and unbonded to + - GlobalStakeShares: Represents shares of `GlobalState.BondedPool` if + `Candidate.Status` is `Bonded`; or shares of `GlobalState.UnbondedPool` if + `Candidate.Status` is otherwise + - IssuedDelegatorShares: Sum of all shares issued to delegators (which + includes the candidate's self-bond) which represent each of 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 the validator is within the top 100 validators. + - Commission: The commission rate of fees charged to any delegators + - CommissionMax: The maximum commission rate which 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 + +validator candidacy can be declared using the `TxDeclareCandidacy` transaction. +During this transaction a self-delegation transaction is executed to bond +tokens which are sent in with the transaction. + +``` golang +type TxDeclareCandidacy struct { + PubKey crypto.PubKey + Amount coin.Coin + GovernancePubKey crypto.PubKey + Commission rational.Rat + CommissionMax int64 + CommissionMaxChange int64 + Description Description +} +``` + +For all subsequent self-bonding, whether self-bonding or delegation the +`TxDelegate` function should be used. In this context `TxUnbond` is used to +unbond either delegation bonds or validator self-bonds. + +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 +} +``` + +### Persistent State + +Within the store, each `Candidate` is stored by validator-pubkey. + + - key: validator-pubkey + - value: `Candidate` object + +A second key-value pair is also persisted in order to quickly sort though the +group of all candidates, this second index is however not persisted through the +merkle store. + + - key: `Candidate.GlobalStakeShares` + - value: `Candidate.PubKey` + +When the set of all validators needs to be determined from the group of all +candidates, the top candidates, sorted by GlobalStakeShares can be retrieved +from this sorting without the need to retrieve the entire group of candidates. +When validators are kicked from the validator set they are removed from this +list. + +### New Validators + +The validator set is updated in the first block of every hour. Validators are +taken as the first `GlobalState.MaxValidators` number of candidates with the +greatest amount of staked atoms who have not been kicked from the validator +set. + +### Kicked Validators + +Unbonding of an entire validator-candidate to a temporary liquid account occurs +under the scenarios: + - not enough stake to be within the validator set + - the owner unbonds all of their staked tokens + - validator liveliness issues + - crosses a self-imposed safety threshold + - minimum number of tokens staked by owner + - minimum ratio of tokens staked by owner to delegator tokens + +When this occurs delegator's tokens do not unbond to their personal wallets but +begin the unbonding process to a pool where they must then transact in order to +withdraw to their respective wallets. The following unbonding will use the +following queue element + +``` golang +type QueueElemUnbondCandidate struct { + QueueElem +} +``` + +If a delegator chooses to initiate an unbond or re-delegation of their shares +while a candidate-unbond is commencing, then that unbond/re-delegation is +subject to a reduced unbonding period based on how much time those funds have +already spent in the unbonding queue. + +#### Liveliness issues + +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`. + +#### Validator Liveliness Proof + +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 livliness is demonstrated +by sending in a `TxProveLive` transaction: + +``` golang +type TxProveLive struct { + PubKey crypto.PubKey +} +``` + +## Delegator bond + +Atom holders may delegate coins to validators, under this circumstance their +funds are held in a `DelegatorBond`. It is owned by one delegator, and is +associated with the shares for one validator. The sender of the transaction is +considered to be the owner of the bond, + +``` golang +type DelegatorBond struct { + Candidate crypto.PubKey + Shares rational.Rat + AdjustmentFeePool coin.Coins + AdjustmentRewardPool coin.Coins +} +``` + +Description: + - Candidate: pubkey of the validator candidate: bonding too + - Shares: the number of 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`` + +Each `DelegatorBond` is individually indexed within the store by delegator +address and candidate pubkey. + + - key: Delegator and Candidate-Pubkey + - value: DelegatorBond + + +### Delegating + +Delegator bonds are created using the TxDelegate transaction. Within this +transaction the validator candidate queried with an amount of coins, whereby +given the current exchange rate of candidate's delegator-shares-to-atoms the +candidate will return shares which are assigned in `DelegatorBond.Shares`. + +``` golang +type TxDelegate struct { + PubKey crypto.PubKey + Amount coin.Coin +} +``` + +### Unbonding + +Delegator unbonding is defined by the following transaction type: + +``` golang +type TxUnbond struct { + PubKey crypto.PubKey + Shares rational.Rat +} +``` + +When unbonding is initiated, delegator shares are immediately removed from the +candidate and added to a queue object. + +``` golang +type QueueElemUnbondDelegation struct { + QueueElem + Payout Address // account to pay out to + Shares rational.Rat // amount of shares which are unbonding + StartSlashRatio rational.Rat // candidate slash ratio at start of re-delegation +} +``` + +In the unbonding queue - the fraction of all historical slashings on +that validator are recorded (`StartSlashRatio`). When this queue reaches maturity +if that total slashing applied is greater on the validator then the +difference (amount that should have been slashed from the first validator) is +assigned to the amount being paid out. + + +### Re-Delegation + +The re-delegation command allows delegators to switch validators while still +receiving equal reward to as if you had never unbonded. + +``` golang +type TxRedelegate struct { + PubKeyFrom crypto.PubKey + PubKeyTo crypto.PubKey + Shares rational.Rat +} +``` + +When re-delegation is initiated, delegator shares remain accounted for within +the `Candidate.Shares`, the term `RedelegatingShares` is incremented and a +queue element is created. + +``` 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 +} +``` + +During the unbonding period all unbonding shares do not count towards the +voting power of a validator. Once the `QueueElemReDelegation` has reached +maturity, the appropriate unbonding shares are removed from the `Shares` and +`RedelegatingShares` term. + +Note that with the current menchanism a delegator cannot redelegate funds which +are currently redelegating. + +### Cancel Unbonding + +A delegator who is in the process of unbonding from a validator may use the +re-delegate transaction to bond back to the original validator they're +currently unbonding from (and only that validator). If initiated, the delegator +will immediately begin to one again collect rewards from their validator. + + +## Provision Calculations + +Every hour atom provisions are assigned proportionally to the each slashable +bonded token which includes re-delegating atoms but not unbonding tokens. + +Validation provisions are payed directly to a global hold account +(`BondedTokenPool`) and proportions of that hold account owned by each +validator is defined as the `GlobalStakeBonded`. The tokens are payed as bonded +tokens. + +Here, the bonded tokens that a candidate has can be calculated as: + +``` +globalStakeExRate = params.BondedTokenPool / params.IssuedGlobalStakeShares +candidateCoins = candidate.GlobalStakeShares * globalStakeExRate +``` + +If a delegator chooses to add more tokens to a validator then the amount of +validator shares distributed is calculated on exchange rate (aka every +delegators shares do not change value at that moment. The validator's +accounting of distributed shares to delegators must also increased at every +deposit. + +``` +delegatorExRate = validatorCoins / candidate.IssuedDelegatorShares +createShares = coinsDeposited / delegatorExRate +candidate.IssuedDelegatorShares += createShares +``` + +Whenever a validator has new tokens added to it, the `BondedTokenPool` is +increased and must be reflected in the global parameter as well as the +validators `GlobalStakeShares`. This calculation ensures that the worth of the +`GlobalStakeShares` of other validators remains worth a constant absolute +amount of the `BondedTokenPool` + +``` +createdGlobalStakeShares = coinsDeposited / globalStakeExRate +validator.GlobalStakeShares += createdGlobalStakeShares +params.IssuedGlobalStakeShares += createdGlobalStakeShares + +params.BondedTokenPool += coinsDeposited +``` + +Similarly, if a delegator wanted to unbond coins: + +``` +coinsWithdrawn = withdrawlShares * delegatorExRate + +destroyedGlobalStakeShares = coinsWithdrawn / globalStakeExRate +validator.GlobalStakeShares -= destroyedGlobalStakeShares +params.IssuedGlobalStakeShares -= destroyedGlobalStakeShares +params.BondedTokenPool -= coinsWithdrawn +``` + +Note that when an re-delegation occurs the shares to move are placed in an +re-delegation queue where they continue to collect validator provisions until +queue element matures. Although provisions are collected during re-delegation, +re-delegation tokens do not contribute to the voting power of a validator. + +Validator provisions are minted on an hourly basis (the first block of a new +hour). The annual target of between 7% and 20%. The long-term target ratio of +bonded tokens to unbonded tokens is 67%. + +The target annual inflation rate is recalculated for each previsions cycle. The +inflation is also subject to a rate change (positive of negative) depending or +the distance from the desired ratio (67%). The maximum rate change possible is +defined to be 13% per year, however the annual inflation is capped as between +7% and 20%. + +``` +inflationRateChange(0) = 0 +annualInflation(0) = 0.07 + +bondedRatio = bondedTokenPool / totalTokenSupply +AnnualInflationRateChange = (1 - bondedRatio / 0.67) * 0.13 + +annualInflation += AnnualInflationRateChange + +if annualInflation > 0.20 then annualInflation = 0.20 +if annualInflation < 0.07 then annualInflation = 0.07 + +provisionTokensHourly = totalTokenSupply * annualInflation / (365.25*24) +``` + +Because the validators hold a relative bonded share (`GlobalStakeShare`), when +more bonded tokens are added proportionally to all validators the only term +which needs to be updated is the `BondedTokenPool`. So for each previsions +cycle: + +``` +params.BondedTokenPool += provisionTokensHourly +``` + +## Fee Calculations + +Collected fees are pooled globally and divided out passively to validators and +delegators. Each validator has the opportunity to charge commission to the +delegators on the fees collected on behalf of the delegators by the validators. +Fees are paid directly into a global fee pool. Due to the nature of of passive +accounting whenever changes to parameters which affect the rate of fee +distribution occurs, withdrawal of fees must also occur. + + - when withdrawing one must withdrawal the maximum amount they are entitled + too, leaving nothing in the pool, + - when bonding, unbonding, or re-delegating tokens to an existing account a + full withdrawal of the fees must occur (as the rules for lazy accounting + change), + - when a candidate chooses to change the commission on fees, all accumulated + commission fees must be simultaneously withdrawn. + +When the validator is the proposer of the round, that validator (and their +delegators) receives between 1% and 5% of fee rewards, the reserve tax is then +charged, then the remainder is distributed socially by voting power to all +validators including the proposer validator. The amount of proposer reward is +calculated from pre-commits Tendermint messages. All provision rewards are +added to a provision reward pool which validator holds individually. Here note +that `BondedShares` represents the sum of all voting power saved in the +`GlobalState` (denoted `gs`). + +``` +proposerReward = feesCollected * (0.01 + 0.04 + * sumOfVotingPowerOfPrecommitValidators / gs.BondedShares) +candidate.ProposerRewardPool += proposerReward + +reserveTaxed = feesCollected * params.ReserveTax +gs.ReservePool += reserveTaxed + +distributedReward = feesCollected - proposerReward - reserveTaxed +gs.FeePool += distributedReward +gs.SumFeesReceived += distributedReward +gs.RecentFee = distributedReward +``` + +The entitlement to the fee pool held by the each validator can be accounted for +lazily. First we must account for a candidate's `count` and `adjustment`. The +`count` represents a lazy accounting of what that candidates entitlement to the +fee pool would be if there `VotingPower` was to never change and they were to +never withdraw fees. + +``` +candidate.count = candidate.VotingPower * BlockHeight +``` + +Similarly the GlobalState count can be passively calculated whenever needed, +where `BondedShares` is the updated sum of voting powers from all validators. + +``` +gs.count = gs.BondedShares * BlockHeight +``` + +The `adjustment` term accounts for changes in voting power and withdrawals of +fees. The adjustment factor must be persisted with the candidate and modified +whenever fees are withdrawn from the candidate or the voting power of the +candidate changes. When the voting power of the candidate changes the +`Adjustment` factor is increased/decreased by the cumulative difference in the +voting power if the voting power has been the new voting power as opposed to +the old voting power for the entire duration of the blockchain up the previous +block. Each time there is an adjustment change the GlobalState (denoted `gs`) +`Adjustment` must also be updated. + +``` +simplePool = candidate.count / gs.count * gs.SumFeesReceived +projectedPool = candidate.PrevPower * (height-1) + / (gs.PrevPower * (height-1)) * gs.PrevFeesReceived + + candidate.Power / gs.Power * gs.RecentFee + +AdjustmentChange = simplePool - projectedPool +candidate.AdjustmentRewardPool += AdjustmentChange +gs.Adjustment += AdjustmentChange +``` + +Every instance that the voting power changes, information about the state of +the validator set during the change must be recorded as a `powerChange` for +other validators to run through. Before any validator modifies its voting power +it must first run through the above calculation to determine the change in +their `caandidate.AdjustmentRewardPool` for all historical changes in the set +of `powerChange` which they have not yet synced to. The set of all +`powerChange` may be trimmed from its oldest members once all validators have +synced past the height of the oldest `powerChange`. This trim procedure will +occur on an epoch basis. + +```golang +type powerChange struct { + height int64 // block height at change + power rational.Rat // total power at change + prevpower rational.Rat // total power at previous height-1 + feesin coins.Coin // fees in at block height + prevFeePool coins.Coin // total fees in at previous block height +} +``` + +Note that the adjustment factor may result as negative if the voting power of a +different candidate has decreased. + +``` +candidate.AdjustmentRewardPool += withdrawn +gs.Adjustment += withdrawn +``` + +Now the entitled fee pool of each candidate can be lazily accounted for at +any given block: + +``` +candidate.feePool = candidate.simplePool - candidate.Adjustment +``` + +So far we have covered two sources fees which can be withdrawn from: Fees from +proposer rewards (`candidate.ProposerRewardPool`), and fees from the fee pool +(`candidate.feePool`). However we should note that all fees from fee pool are +subject to commission rate from the owner of the candidate. These next +calculations outline the math behind withdrawing fee rewards as either a +delegator to a candidate providing commission, or as the owner of a candidate +who is receiving commission. + +### Calculations For Delegators and Candidates + +The same mechanism described to calculate the fees which an entire validator is +entitled to is be applied to delegator level to determine the entitled fees for +each delegator and the candidates entitled commission from `gs.FeesPool` and +`candidate.ProposerRewardPool`. + +The calculations are identical with a few modifications to the parameters: + - Delegator's entitlement to `gs.FeePool`: + - entitled party voting power should be taken as the effective voting power + after commission is retrieved, + `bond.Shares/candidate.TotalDelegatorShares * candidate.VotingPower * (1 - candidate.Commission)` + - Delegator's entitlement to `candidate.ProposerFeePool` + - global power in this context is actually shares + `candidate.TotalDelegatorShares` + - entitled party voting power should be taken as the effective shares after + commission is retrieved, `bond.Shares * (1 - candidate.Commission)` + - Candidate's commission entitlement to `gs.FeePool` + - entitled party voting power should be taken as the effective voting power + of commission portion of total voting power, + `candidate.VotingPower * candidate.Commission` + - Candidate's commission entitlement to `candidate.ProposerFeePool` + - global power in this context is actually shares + `candidate.TotalDelegatorShares` + - entitled party voting power should be taken as the of commission portion + of total delegators shares, + `candidate.TotalDelegatorShares * candidate.Commission` + +For more implementation ideas see spreadsheet `spec/AbsoluteFeeDistrModel.xlsx` + +As mentioned earlier, every time the voting power of a delegator bond is +changing either by unbonding or further bonding, all fees must be +simultaneously withdrawn. Similarly if the validator changes the commission +rate, all commission on fees must be simultaneously withdrawn. + +### Other general notes on fees accounting + +- When a delegator chooses to re-delegate shares, fees continue to accumulate + until the re-delegation queue reaches maturity. At the block which the queue + reaches maturity and shares are re-delegated all available fees are + simultaneously withdrawn. +- Whenever a totally new validator is added to the validator set, the `accum` + of the entire candidate must be 0, meaning that the initial value for + `candidate.Adjustment` must be set to the value of `canidate.Count` for the + height which the candidate is added on the validator set. +- The feePool of a new delegator bond will be 0 for the height at which the bond + was added. This is achieved by setting `DelegatorBond.FeeWithdrawalHeight` to + the height which the bond was added. diff --git a/docs/spec/staking/old/spec2.md b/docs/spec/staking/old/spec2.md new file mode 100644 index 000000000000..72bb8a2e371d --- /dev/null +++ b/docs/spec/staking/old/spec2.md @@ -0,0 +1,698 @@ +# Stake Module + +## Overview + +The stake module is tasked with various core staking functionality, +including validator set rotation, unbonding periods, and the +distribution of inflationary provisions and transaction fees. +It is designed to efficiently facilitate small numbers of +validators (hundreds), and large numbers of delegators (tens of thousands). + +Bonded Atoms are pooled globally and for each validator. +Validators have shares in the global pool, and delegators +have shares in the pool of every validator they delegate to. +Atom provisions simply accumulate in the global pool, making +each share worth proportionally more. + +Validator shares can be redeemed for Atoms, but the Atoms will be locked in a queue +for an unbonding period before they can be withdrawn to an account. +Delegators can exchange one validator's shares for another immediately +(ie. they can re-delegate to another validator), but must then wait the +unbonding period before they can do it again. + +Fees are pooled separately and withdrawn lazily, at any time. +They are not bonded, and can be paid in multiple tokens. +An adjustment factor is maintained for each validator +and delegator to determine the true proportion of fees in the pool they are entitled too. +Adjustment factors are updated every time a validator or delegator's voting power changes. +Validators and delegators must withdraw all fees they are entitled too before they can bond or +unbond Atoms. + +## State + +The staking module persists the following to the store: +- `GlobalState`, describing the global pools +- a `Candidate` for each candidate validator, indexed by public key +- a `Candidate` for each candidate validator, indexed by shares in the global pool (ie. ordered) +- a `DelegatorBond` for each delegation to a candidate by a delegator, indexed by delegator and candidate + public keys +- a `Queue` of unbonding delegations (TODO) + +### Global State + +``` golang +type GlobalState struct { + TotalSupply int64 // total supply of atom tokens + BondedShares rational.Rat // sum of all shares distributed for the BondedPool + UnbondedShares rational.Rat // sum of all shares distributed for the UnbondedPool + BondedPool int64 // reserve of bonded tokens + UnbondedPool int64 // reserve of unbonded tokens held with candidates + 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 +} +``` + +### Candidate + +The `Candidate` struct holds the current state and some historical actions of +validators or candidate-validators. + +``` golang +type Candidate struct { + Status CandidateStatus + PubKey crypto.PubKey + GovernancePubKey crypto.PubKey + Owner 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 CandidateStatus byte +const ( + VyingUnbonded CandidateStatus = 0x00 + VyingUnbonding CandidateStatus = 0x01 + Bonded CandidateStatus = 0x02 + KickUnbonding CandidateStatus = 0x03 + KickUnbonded CandidateStatus = 0x04 +) + +type Description struct { + Name string + DateBonded string + Identity string + Website string + Details string +} +``` + +Candidate parameters are described: + - Status: signal that the candidate is either vying for validator status + either unbonded or unbonding, an active validator, or a kicked validator + either unbonding or unbonded. + - PubKey: separated key from the owner of the candidate as is used strictly + for participating in consensus. + - Owner: Address where coins are bonded from and unbonded to + - GlobalStakeShares: Represents shares of `GlobalState.BondedPool` if + `Candidate.Status` is `Bonded`; or shares of `GlobalState.UnbondedPool` if + `Candidate.Status` is otherwise + - IssuedDelegatorShares: Sum of all shares issued to delegators (which + includes the candidate's self-bond) which represent each of 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 the validator is within the top 100 validators. + - Commission: The commission rate of fees charged to any delegators + - CommissionMax: The maximum commission rate which 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 + + +Candidates are indexed by their `Candidate.PubKey`. +Additionally, we index empty values by the candidates global stake shares concatenated with the public key. + +TODO: be more precise. + +When the set of all validators needs to be determined from the group of all +candidates, the top candidates, sorted by GlobalStakeShares can be retrieved +from this sorting without the need to retrieve the entire group of candidates. +When validators are kicked from the validator set they are removed from this +list. + + +### DelegatorBond + +Atom holders may delegate coins to validators, under this circumstance their +funds are held in a `DelegatorBond`. It is owned by one delegator, and is +associated with the shares for one validator. The sender of the transaction is +considered to be the owner of the bond, + +``` golang +type DelegatorBond struct { + Candidate crypto.PubKey + Shares rational.Rat + AdjustmentFeePool coin.Coins + AdjustmentRewardPool coin.Coins +} +``` + +Description: + - Candidate: pubkey of the validator candidate: bonding too + - Shares: the number of 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`` + +Each `DelegatorBond` is individually indexed within the store by delegator +address and candidate pubkey. + + - key: Delegator and Candidate-Pubkey + - value: DelegatorBond + + +### Unbonding Queue + + +- main unbonding queue contains both UnbondElem and RedelegateElem + - "queue" + +- new unbonding queue every time a val leaves the validator set + - "queue"+ + + + + + + + + +The queue is ordered so the next 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 `InitHeight` commence with final settlement of the unbonding and pop the +queue. All queue elements used for unbonding share a common struct: + +``` golang +type QueueElem struct { + Candidate crypto.PubKey + InitHeight int64 // when the queue was initiated +} +``` + +``` golang +type QueueElemUnbondCandidate struct { + QueueElem +} +``` + + + +``` golang +type QueueElemUnbondDelegation struct { + QueueElem + Payout Address // account to pay out to + Shares rational.Rat // amount of shares which are unbonding + StartSlashRatio rational.Rat // candidate slash ratio at start of re-delegation +} +``` + + + +``` 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 +} +``` + + +Each `QueueElem` is persisted in the store until it is popped from the queue. + +## Transactions + +### TxDeclareCandidacy + +Validator candidacy can be declared using the `TxDeclareCandidacy` transaction. +During this transaction a self-delegation transaction is executed to bond +tokens which are sent in with the transaction. + +``` golang +type TxDeclareCandidacy struct { + PubKey crypto.PubKey + Amount coin.Coin + GovernancePubKey crypto.PubKey + Commission rational.Rat + CommissionMax int64 + CommissionMaxChange int64 + Description Description +} +``` + +### 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 +} +``` + + +### TxLivelinessCheck + +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 livliness is demonstrated +by sending in a `TxProveLive` transaction: + +``` golang +type TxProveLive struct { + PubKey crypto.PubKey +} +``` + + +### TxDelegate + +All bonding, whether self-bonding or delegation, is done via +`TxDelegate`. + +Delegator bonds are created using the TxDelegate transaction. Within this +transaction the validator candidate queried with an amount of coins, whereby +given the current exchange rate of candidate's delegator-shares-to-atoms the +candidate will return shares which are assigned in `DelegatorBond.Shares`. + +``` golang +type TxDelegate struct { + PubKey crypto.PubKey + Amount coin.Coin +} +``` + +### TxUnbond + + +In this context `TxUnbond` is used to +unbond either delegation bonds or validator self-bonds. + +Delegator unbonding is defined by the following transaction type: + +``` golang +type TxUnbond struct { + PubKey crypto.PubKey + Shares rational.Rat +} +``` + + +### TxRedelegate + +The re-delegation command allows delegators to switch validators while still +receiving equal reward to as if you had never unbonded. + +``` golang +type TxRedelegate struct { + PubKeyFrom crypto.PubKey + PubKeyTo crypto.PubKey + Shares rational.Rat + +} +``` + +A delegator who is in the process of unbonding from a validator may use the +re-delegate transaction to bond back to the original validator they're +currently unbonding from (and only that validator). If initiated, the delegator +will immediately begin to one again collect rewards from their validator. + +### TxWithdraw + +.... + + +## EndBlock + +### Update Validators + +The validator set is updated in the first block of every hour. Validators are +taken as the first `GlobalState.MaxValidators` number of candidates with the +greatest amount of staked atoms who have not been kicked from the validator +set. + +Unbonding of an entire validator-candidate to a temporary liquid account occurs +under the scenarios: + - not enough stake to be within the validator set + - the owner unbonds all of their staked tokens + - validator liveliness issues + - crosses a self-imposed safety threshold + - minimum number of tokens staked by owner + - minimum ratio of tokens staked by owner to delegator tokens + +When this occurs delegator's tokens do not unbond to their personal wallets but +begin the unbonding process to a pool where they must then transact in order to +withdraw to their respective wallets. + +### Unbonding + +When unbonding is initiated, delegator shares are immediately removed from the +candidate and added to a queue object. + +In the unbonding queue - the fraction of all historical slashings on +that validator are recorded (`StartSlashRatio`). When this queue reaches maturity +if that total slashing applied is greater on the validator then the +difference (amount that should have been slashed from the first validator) is +assigned to the amount being paid out. + + +#### Liveliness issues + +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 + + +## Invariants + +----------------------------- + +------------ + + + + +If a delegator chooses to initiate an unbond or re-delegation of their shares +while a candidate-unbond is commencing, then that unbond/re-delegation is +subject to a reduced unbonding period based on how much time those funds have +already spent in the unbonding queue. + +### Re-Delegation + +When re-delegation is initiated, delegator shares remain accounted for within +the `Candidate.Shares`, the term `RedelegatingShares` is incremented and a +queue element is created. + +During the unbonding period all unbonding shares do not count towards the +voting power of a validator. Once the `QueueElemReDelegation` has reached +maturity, the appropriate unbonding shares are removed from the `Shares` and +`RedelegatingShares` term. + +Note that with the current menchanism a delegator cannot redelegate funds which +are currently redelegating. + +---------------------------------------------- + +## Provision Calculations + +Every hour atom provisions are assigned proportionally to the each slashable +bonded token which includes re-delegating atoms but not unbonding tokens. + +Validation provisions are payed directly to a global hold account +(`BondedTokenPool`) and proportions of that hold account owned by each +validator is defined as the `GlobalStakeBonded`. The tokens are payed as bonded +tokens. + +Here, the bonded tokens that a candidate has can be calculated as: + +``` +globalStakeExRate = params.BondedTokenPool / params.IssuedGlobalStakeShares +candidateCoins = candidate.GlobalStakeShares * globalStakeExRate +``` + +If a delegator chooses to add more tokens to a validator then the amount of +validator shares distributed is calculated on exchange rate (aka every +delegators shares do not change value at that moment. The validator's +accounting of distributed shares to delegators must also increased at every +deposit. + +``` +delegatorExRate = validatorCoins / candidate.IssuedDelegatorShares +createShares = coinsDeposited / delegatorExRate +candidate.IssuedDelegatorShares += createShares +``` + +Whenever a validator has new tokens added to it, the `BondedTokenPool` is +increased and must be reflected in the global parameter as well as the +validators `GlobalStakeShares`. This calculation ensures that the worth of the +`GlobalStakeShares` of other validators remains worth a constant absolute +amount of the `BondedTokenPool` + +``` +createdGlobalStakeShares = coinsDeposited / globalStakeExRate +validator.GlobalStakeShares += createdGlobalStakeShares +params.IssuedGlobalStakeShares += createdGlobalStakeShares + +params.BondedTokenPool += coinsDeposited +``` + +Similarly, if a delegator wanted to unbond coins: + +``` +coinsWithdrawn = withdrawlShares * delegatorExRate + +destroyedGlobalStakeShares = coinsWithdrawn / globalStakeExRate +validator.GlobalStakeShares -= destroyedGlobalStakeShares +params.IssuedGlobalStakeShares -= destroyedGlobalStakeShares +params.BondedTokenPool -= coinsWithdrawn +``` + +Note that when an re-delegation occurs the shares to move are placed in an +re-delegation queue where they continue to collect validator provisions until +queue element matures. Although provisions are collected during re-delegation, +re-delegation tokens do not contribute to the voting power of a validator. + +Validator provisions are minted on an hourly basis (the first block of a new +hour). The annual target of between 7% and 20%. The long-term target ratio of +bonded tokens to unbonded tokens is 67%. + +The target annual inflation rate is recalculated for each previsions cycle. The +inflation is also subject to a rate change (positive of negative) depending or +the distance from the desired ratio (67%). The maximum rate change possible is +defined to be 13% per year, however the annual inflation is capped as between +7% and 20%. + +``` +inflationRateChange(0) = 0 +annualInflation(0) = 0.07 + +bondedRatio = bondedTokenPool / totalTokenSupply +AnnualInflationRateChange = (1 - bondedRatio / 0.67) * 0.13 + +annualInflation += AnnualInflationRateChange + +if annualInflation > 0.20 then annualInflation = 0.20 +if annualInflation < 0.07 then annualInflation = 0.07 + +provisionTokensHourly = totalTokenSupply * annualInflation / (365.25*24) +``` + +Because the validators hold a relative bonded share (`GlobalStakeShare`), when +more bonded tokens are added proportionally to all validators the only term +which needs to be updated is the `BondedTokenPool`. So for each previsions +cycle: + +``` +params.BondedTokenPool += provisionTokensHourly +``` + +## Fee Calculations + +Collected fees are pooled globally and divided out passively to validators and +delegators. Each validator has the opportunity to charge commission to the +delegators on the fees collected on behalf of the delegators by the validators. +Fees are paid directly into a global fee pool. Due to the nature of of passive +accounting whenever changes to parameters which affect the rate of fee +distribution occurs, withdrawal of fees must also occur. + + - when withdrawing one must withdrawal the maximum amount they are entitled + too, leaving nothing in the pool, + - when bonding, unbonding, or re-delegating tokens to an existing account a + full withdrawal of the fees must occur (as the rules for lazy accounting + change), + - when a candidate chooses to change the commission on fees, all accumulated + commission fees must be simultaneously withdrawn. + +When the validator is the proposer of the round, that validator (and their +delegators) receives between 1% and 5% of fee rewards, the reserve tax is then +charged, then the remainder is distributed socially by voting power to all +validators including the proposer validator. The amount of proposer reward is +calculated from pre-commits Tendermint messages. All provision rewards are +added to a provision reward pool which validator holds individually. Here note +that `BondedShares` represents the sum of all voting power saved in the +`GlobalState` (denoted `gs`). + +``` +proposerReward = feesCollected * (0.01 + 0.04 + * sumOfVotingPowerOfPrecommitValidators / gs.BondedShares) +candidate.ProposerRewardPool += proposerReward + +reserveTaxed = feesCollected * params.ReserveTax +gs.ReservePool += reserveTaxed + +distributedReward = feesCollected - proposerReward - reserveTaxed +gs.FeePool += distributedReward +gs.SumFeesReceived += distributedReward +gs.RecentFee = distributedReward +``` + +The entitlement to the fee pool held by the each validator can be accounted for +lazily. First we must account for a candidate's `count` and `adjustment`. The +`count` represents a lazy accounting of what that candidates entitlement to the +fee pool would be if there `VotingPower` was to never change and they were to +never withdraw fees. + +``` +candidate.count = candidate.VotingPower * BlockHeight +``` + +Similarly the GlobalState count can be passively calculated whenever needed, +where `BondedShares` is the updated sum of voting powers from all validators. + +``` +gs.count = gs.BondedShares * BlockHeight +``` + +The `adjustment` term accounts for changes in voting power and withdrawals of +fees. The adjustment factor must be persisted with the candidate and modified +whenever fees are withdrawn from the candidate or the voting power of the +candidate changes. When the voting power of the candidate changes the +`Adjustment` factor is increased/decreased by the cumulative difference in the +voting power if the voting power has been the new voting power as opposed to +the old voting power for the entire duration of the blockchain up the previous +block. Each time there is an adjustment change the GlobalState (denoted `gs`) +`Adjustment` must also be updated. + +``` +simplePool = candidate.count / gs.count * gs.SumFeesReceived +projectedPool = candidate.PrevPower * (height-1) + / (gs.PrevPower * (height-1)) * gs.PrevFeesReceived + + candidate.Power / gs.Power * gs.RecentFee + +AdjustmentChange = simplePool - projectedPool +candidate.AdjustmentRewardPool += AdjustmentChange +gs.Adjustment += AdjustmentChange +``` + +Every instance that the voting power changes, information about the state of +the validator set during the change must be recorded as a `powerChange` for +other validators to run through. Before any validator modifies its voting power +it must first run through the above calculation to determine the change in +their `caandidate.AdjustmentRewardPool` for all historical changes in the set +of `powerChange` which they have not yet synced to. The set of all +`powerChange` may be trimmed from its oldest members once all validators have +synced past the height of the oldest `powerChange`. This trim procedure will +occur on an epoch basis. + +```golang +type powerChange struct { + height int64 // block height at change + power rational.Rat // total power at change + prevpower rational.Rat // total power at previous height-1 + feesin coins.Coin // fees in at block height + prevFeePool coins.Coin // total fees in at previous block height +} +``` + +Note that the adjustment factor may result as negative if the voting power of a +different candidate has decreased. + +``` +candidate.AdjustmentRewardPool += withdrawn +gs.Adjustment += withdrawn +``` + +Now the entitled fee pool of each candidate can be lazily accounted for at +any given block: + +``` +candidate.feePool = candidate.simplePool - candidate.Adjustment +``` + +So far we have covered two sources fees which can be withdrawn from: Fees from +proposer rewards (`candidate.ProposerRewardPool`), and fees from the fee pool +(`candidate.feePool`). However we should note that all fees from fee pool are +subject to commission rate from the owner of the candidate. These next +calculations outline the math behind withdrawing fee rewards as either a +delegator to a candidate providing commission, or as the owner of a candidate +who is receiving commission. + +### Calculations For Delegators and Candidates + +The same mechanism described to calculate the fees which an entire validator is +entitled to is be applied to delegator level to determine the entitled fees for +each delegator and the candidates entitled commission from `gs.FeesPool` and +`candidate.ProposerRewardPool`. + +The calculations are identical with a few modifications to the parameters: + - Delegator's entitlement to `gs.FeePool`: + - entitled party voting power should be taken as the effective voting power + after commission is retrieved, + `bond.Shares/candidate.TotalDelegatorShares * candidate.VotingPower * (1 - candidate.Commission)` + - Delegator's entitlement to `candidate.ProposerFeePool` + - global power in this context is actually shares + `candidate.TotalDelegatorShares` + - entitled party voting power should be taken as the effective shares after + commission is retrieved, `bond.Shares * (1 - candidate.Commission)` + - Candidate's commission entitlement to `gs.FeePool` + - entitled party voting power should be taken as the effective voting power + of commission portion of total voting power, + `candidate.VotingPower * candidate.Commission` + - Candidate's commission entitlement to `candidate.ProposerFeePool` + - global power in this context is actually shares + `candidate.TotalDelegatorShares` + - entitled party voting power should be taken as the of commission portion + of total delegators shares, + `candidate.TotalDelegatorShares * candidate.Commission` + +For more implementation ideas see spreadsheet `spec/AbsoluteFeeDistrModel.xlsx` + +As mentioned earlier, every time the voting power of a delegator bond is +changing either by unbonding or further bonding, all fees must be +simultaneously withdrawn. Similarly if the validator changes the commission +rate, all commission on fees must be simultaneously withdrawn. + +### Other general notes on fees accounting + +- When a delegator chooses to re-delegate shares, fees continue to accumulate + until the re-delegation queue reaches maturity. At the block which the queue + reaches maturity and shares are re-delegated all available fees are + simultaneously withdrawn. +- Whenever a totally new validator is added to the validator set, the `accum` + of the entire candidate must be 0, meaning that the initial value for + `candidate.Adjustment` must be set to the value of `canidate.Count` for the + height which the candidate is added on the validator set. +- The feePool of a new delegator bond will be 0 for the height at which the bond + was added. This is achieved by setting `DelegatorBond.FeeWithdrawalHeight` to + the height which the bond was added. diff --git a/docs/spec/staking/spec-technical.md b/docs/spec/staking/spec-technical.md new file mode 100644 index 000000000000..e3a528d948b5 --- /dev/null +++ b/docs/spec/staking/spec-technical.md @@ -0,0 +1,485 @@ +# 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 for it's service of securing blockchain network rewarded by the inflationary +provisions and transactions fees. This incentivizes correct behavior of the validators and provide 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 holder 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 securing network (and taking a risk of being slashed in case the +validator misbehaves), 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`, describing the global pools and the inflation related fields +- `map[PubKey]Candidate`, a map of validator candidates (including current validators), indexed by public key +- `map[rational.Rat]Candidate`, an ordered map of validator candidates (including current validators), indexed by +shares in the global pool (bonded or unbonded depending on candidate status) +- `map[[]byte]DelegatorBond`, a map of DelegatorBonds (for each delegation to a candidate by a delegator), indexed by +the delegator address and the candidate public key +- `queue[QueueElemUnbondDelegation]`, a queue of unbonding delegations +- `queue[QueueElemReDelegate]`, a queue of re-delegations + +### Global State + +GlobalState data structure contains total Atoms supply, amount of Atoms in the bonded pool, sum of all shares +distributed for the bonded pool, amount of Atoms in the unbonded pool, sum of all shares distributed for the +unbonded pool, a timestamp of the last processing of inflation, the current annual inflation rate, a timestamp +for the last comission accounting reset, the global fee pool, a pool of reserve taxes collected for the governance use +and an adjustment factor for calculating global feel accum (?). + +``` golang +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 unbonded 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 +} +``` + +### Candidate + +The `Candidate` data structure holds the current state and some historical actions of +validators or candidate-validators. + +``` golang +type Candidate struct { + Status CandidateStatus + PubKey crypto.PubKey + GovernancePubKey crypto.PubKey + Owner 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 +} +``` + +CandidateStatus can be VyingUnbonded, VyingUnbonding, Bonded, KickUnbonding and KickUnbonded. + + +``` golang +type Description struct { + Name string + DateBonded string + Identity string + Website string + Details string +} +``` + +Candidate parameters are described: + - Status: signal that the candidate is either vying for validator status, + either unbonded or unbonding, an active validator, or a kicked validator + either unbonding or unbonded. + - PubKey: separated key from the owner of the candidate as is used strictly + for participating in consensus. + - Owner: Address where coins are bonded from and unbonded to + - GlobalStakeShares: Represents shares of `GlobalState.BondedPool` if + `Candidate.Status` is `Bonded`; or shares of `GlobalState.UnbondedPool` 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 the candidate is a validator. + - 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 validators; 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 validator. The sender of the transaction is +considered the owner of the bond. + +``` golang +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 + +Unbonding and re-delegation process is implemented using the ordered queue data structure. +All queue elements used share a common structure: + +``` golang +type QueueElem struct { + Candidate crypto.PubKey + InitHeight int64 // when the queue was initiated +} +``` + +The queue is ordered so the next 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 `InitHeight`, the final settlement of the unbonding is started or re-delegation is executed, and the element is +pop from the queue. Each `QueueElem` is persisted in the store until it is popped from the queue. + +### QueueElemUnbondDelegation + +``` golang +type QueueElemUnbondDelegation struct { + QueueElem + Payout Address // account to pay out to + Shares rational.Rat // amount of delegator shares which are unbonding + StartSlashRatio rational.Rat // candidate slash ratio at start of re-delegation +} +``` +In the unbonding queue - the fraction of all historical slashings on +that validator are recorded (`StartSlashRatio`). When this queue reaches maturity +if that total slashing applied is greater on the validator then the +difference (amount that should have been slashed from the first validator) is +assigned to the amount being paid out. + +### QueueElemReDelegate + +``` 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 + - TxLivelinessCheck + - TxProveLive + - TxDelegate + - TxUnbond + - TxRedelegate + +## Transaction processing + +In this section we describe the processing of the transactions and the corresponding updates to the global state. +For the following text we will use gs to refer to the GlobalState data structure, candidateMap is a reference to the +map[PubKey]Candidate, delegatorBonds is a reference to map[[]byte]DelegatorBond, unbondDelegationQueue is a +reference to the queue[QueueElemUnbondDelegation] and redelegationQueue is the reference for the +queue[QueueElemReDelegate]. We use tx to denote reference to a transaction that is being processed. + +### TxDeclareCandidacy + +A validator candidacy can be declared using the `TxDeclareCandidacy` transaction. +During this transaction a self-delegation transaction is executed to bond +tokens which are sent in with the transaction (TODO: What does this mean?). + +``` golang +type TxDeclareCandidacy struct { + PubKey crypto.PubKey + Amount coin.Coin + GovernancePubKey crypto.PubKey + Commission rational.Rat + CommissionMax int64 + CommissionMaxChange int64 + Description Description +} +``` + +``` +declareCandidacy(tx TxDeclareCandidacy): + // create and save the empty candidate + candidate = loadCandidate(store, tx.PubKey) + if candidate != nil then return + + 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) + + // move coins from the sender account to a (self-bond) delegator account + // the candidate account and global shares are updated within here + txDelegate = TxDelegate{tx.BondUpdate} + return delegateWithCandidate(txDelegate, candidate) +``` + +### 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 == Unbonded return + if tx.GovernancePubKey != nil then candidate.GovernancePubKey = tx.GovernancePubKey + if tx.Commission >= 0 then candidate.Commission = tx.Commission + if tx.Description != nil then candidate.Description = tx.Description + saveCandidate(store, candidate) + return + ``` + +### TxDelegate + +All bonding, whether self-bonding or delegation, is done via `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`. The amount of created delegator shares depends on the candidate's +delegator-shares-to-atoms exchange rate and is computed as +`delegator-shares = delegator-coins / delegator-shares-to-atom-ex-rate`. + +``` golang +type TxDelegate struct { + PubKey crypto.PubKey + Amount coin.Coin +} +``` + +``` +delegate(tx TxDelegate): + candidate = loadCandidate(store, tx.PubKey) + if candidate == nil then return + return delegateWithCandidate(tx, candidate) + +delegateWithCandidate(tx TxDelegate, candidate Candidate): + if candidate.Status == Revoked then return + + if candidate.Status == Bonded then + poolAccount = address of the bonded pool + else + poolAccount = address of the unbonded pool + + // Move coins from the delegator account to the bonded pool account + err = transfer(sender, poolAccount, tx.Amount) + if err != nil then return + + // Get or create the delegator bond + bond = loadDelegatorBond(store, sender, tx.PubKey) + if bond == nil then + bond = DelegatorBond{tx.PubKey,rational.Zero, Coin(0), Coin(0)} + + issuedDelegatorShares = candidate.addTokens(tx.Amount, gs) + bond.Shares = bond.Shares.Add(issuedDelegatorShares) + + saveCandidate(store, candidate) + + store.Set(GetDelegatorBondKey(sender, bond.PubKey), bond) + + saveGlobalState(store, gs) + return + +addTokens(amount int64, gs GlobalState, candidate Candidate): + + // get the exchange rate of global pool shares over delegator shares + if candidate.IssuedDelegatorShares.IsZero() then + exRate = rational.One + else + exRate = candiate.GlobalStakeShares.Quo(candidate.IssuedDelegatorShares) + + if candidate.Status == Bonded then + gs.BondedPool += amount + issuedShares = exchangeRate(gs.BondedShares, gs.BondedPool).Inv().Mul(amount) // (tokens/shares)^-1 * tokens + gs.BondedShares = gs.BondedShares.Add(issuedShares) + else + gs.UnbondedPool += amount + issuedShares = exchangeRate(gs.UnbondedShares, gs.UnbondedPool).Inv().Mul(amount) // (tokens/shares)^-1 * tokens + gs.UnbondedShares = gs.UnbondedShares.Add(issuedShares) + + candidate.GlobalStakeShares = candidate.GlobalStakeShares.Add(issuedShares) + + issuedDelegatorShares = exRate.Mul(receivedGlobalShares) + candidate.IssuedDelegatorShares = candidate.IssuedDelegatorShares.Add(issuedDelegatorShares) + return + +exchangeRate(shares rational.Rat, tokenAmount int64): + if shares.IsZero() then return rational.One + return shares.Inv().Mul(tokenAmount) + +``` + +### TxUnbond +Delegator unbonding is defined with the following transaction: + +``` golang +type TxUnbond struct { + PubKey crypto.PubKey + Shares rational.Rat +} +``` + +``` +unbond(tx TxUnbond): + + // get delegator bond + bond = loadDelegatorBond(store, sender, tx.PubKey) + if bond == nil then return + + // subtract bond tokens from delegator bond + if bond.Shares.LT(tx.Shares) return // bond shares < tx shares + + bond.Shares = bond.Shares.Sub(ts.Shares) + + candidate = loadCandidate(store, tx.PubKey) + if candidate == nil return + + revokeCandidacy = false + if bond.Shares.IsZero() { + // if the bond is the owner of the candidate then trigger a revoke candidacy + if sender.Equals(candidate.Owner) and candidate.Status != Revoked then + revokeCandidacy = true + + // remove the bond + removeDelegatorBond(store, sender, tx.PubKey) + else + saveDelegatorBond(store, sender, bond) + + // transfer coins back to account + if candidate.Status == Bonded then + poolAccount = address of the bonded pool + else + poolAccount = address of the unbonded pool + + returnCoins = candidate.removeShares(shares, gs) + // TODO: Shouldn't it be created a queue element in this case? + transfer(poolAccount, sender, returnCoins) + + if revokeCandidacy then + // change the share types to unbonded if they were not already + if candidate.Status == Bonded then + // replace bonded shares with unbonded shares + tokens = gs.removeSharesBonded(candidate.GlobalStakeShares) + candidate.GlobalStakeShares = gs.addTokensUnbonded(tokens) + candidate.Status = Unbonded + + transfer(address of the bonded pool, address of the unbonded pool, tokens) + // lastly update the status + candidate.Status = Revoked + + // deduct shares from the candidate and save + if candidate.GlobalStakeShares.IsZero() then + removeCandidate(store, tx.PubKey) + else + saveCandidate(store, candidate) + + saveGlobalState(store, gs) + return + +removeDelegatorBond(candidate Candidate): + + // first remove from the list of bonds + pks = loadDelegatorCandidates(store, sender) + for i, pk := range pks { + if candidate.Equals(pk) { + pks = append(pks[:i], pks[i+1:]...) + } + } + b := wire.BinaryBytes(pks) + store.Set(GetDelegatorBondsKey(delegator), b) + + // now remove the actual bond + store.Remove(GetDelegatorBondKey(delegator, candidate)) + //updateDelegatorBonds(store, delegator) +} +``` + +### Inflation provisions + +Validator provisions are minted on an hourly basis (the first block of a new +hour). The annual target of between 7% and 20%. The long-term target ratio of +bonded tokens to unbonded tokens is 67%. + +The target annual inflation rate is recalculated for each previsions cycle. The +inflation is also subject to a rate change (positive of negative) depending or +the distance from the desired ratio (67%). The maximum rate change possible is +defined to be 13% per year, however the annual inflation is capped as between +7% and 20%. + +``` +inflationRateChange(0) = 0 +GlobalState.Inflation(0) = 0.07 + +bondedRatio = GlobalState.BondedPool / GlobalState.TotalSupply +AnnualInflationRateChange = (1 - bondedRatio / 0.67) * 0.13 + +annualInflation += AnnualInflationRateChange + +if annualInflation > 0.20 then GlobalState.Inflation = 0.20 +if annualInflation < 0.07 then GlobalState.Inflation = 0.07 + +provisionTokensHourly = GlobalState.TotalSupply * GlobalState.Inflation / (365.25*24) +``` + +Because the validators hold a relative bonded share (`GlobalStakeShares`), when +more bonded tokens are added proportionally to all validators, the only term +which needs to be updated is the `GlobalState.BondedPool`. So for each previsions +cycle: + +``` +GlobalState.BondedPool += provisionTokensHourly +```