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