porting staking, wip

wip
This commit is contained in:
rigelrozanski 2018-02-23 23:57:31 +00:00
parent c8c85dfbc8
commit 80d88c3a4c
5 changed files with 1141 additions and 0 deletions

103
x/stake/errors.go Normal file
View File

@ -0,0 +1,103 @@
// nolint
package stake
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
type CodeType = sdk.CodeType
const (
// Gaia errors reserve 200 ~ 299.
CodeInvalidValidator CodeType = 201
CodeInvalidCandidate CodeType = 202
CodeInvalidBond CodeType = 203
CodeInvalidInput CodeType = 204
CodeUnauthorized CodeType = sdk.CodeUnauthorized
CodeInternal CodeType = sdk.CodeInternal
CodeUnknownRequest CodeType = sdk.CodeUnknownRequest
)
// NOTE: Don't stringer this, we'll put better messages in later.
func codeToDefaultMsg(code CodeType) string {
switch code {
case CodeInvalidValidator:
return "Invalid Validator"
case CodeInvalidCandidate:
return "Invalid Candidate"
case CodeInvalidBond:
return "Invalid Bond"
case CodeInvalidInput:
return "Invalid Input"
case CodeUnauthorized:
return "Unauthorized"
case CodeInternal:
return "Internal Error"
case CodeUnknownRequest:
return "Unknown request"
default:
return sdk.CodeToDefaultMsg(code)
}
}
//----------------------------------------
// Error constructors
func ErrCandidateEmpty() error {
return newError(CodeInvalidValidator, "Cannot bond to an empty candidate")
}
func ErrBadBondingDenom() error {
return newError(CodeInvalidValidator, "Invalid coin denomination")
}
func ErrBadBondingAmount() error {
return newError(CodeInvalidValidator, "Amount must be > 0")
}
func ErrNoBondingAcct() error {
return newError(CodeInvalidValidator, "No bond account for this (address, validator) pair")
}
func ErrCommissionNegative() error {
return newError(CodeInvalidValidator, "Commission must be positive")
}
func ErrCommissionHuge() error {
return newError(CodeInvalidValidator, "Commission cannot be more than 100%")
}
func ErrBadValidatorAddr() error {
return newError(CodeInvalidValidator, "Validator does not exist for that address")
}
func ErrCandidateExistsAddr() error {
return newError(CodeInvalidValidator, "Candidate already exist, cannot re-declare candidacy")
}
func ErrMissingSignature() error {
return newError(CodeInvalidValidator, "Missing signature")
}
func ErrBondNotNominated() error {
return newError(CodeInvalidValidator, "Cannot bond to non-nominated account")
}
func ErrNoCandidateForAddress() error {
return newError(CodeInvalidValidator, "Validator does not exist for that address")
}
func ErrNoDelegatorForAddress() error {
return newError(CodeInvalidValidator, "Delegator does not contain validator bond")
}
func ErrInsufficientFunds() error {
return newError(CodeInvalidValidator, "Insufficient bond shares")
}
func ErrBadRemoveValidator() error {
return newError(CodeInvalidValidator, "Error removing validator")
}
//----------------------------------------
// TODO group with code from x/bank/errors.go
func msgOrDefaultMsg(msg string, code CodeType) string {
if msg != "" {
return msg
}
return codeToDefaultMsg(code)
}
func newError(code CodeType, msg string) sdk.Error {
msg = msgOrDefaultMsg(msg, code)
return sdk.NewError(code, msg)
}

520
x/stake/handler.go Normal file
View File

@ -0,0 +1,520 @@
package stake
import (
"fmt"
"strconv"
"github.com/spf13/viper"
"github.com/tendermint/tmlibs/log"
"github.com/tendermint/tmlibs/rational"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
coin "github.com/cosmos/cosmos-sdk/x/bank" // XXX fix
)
// nolint
const stakingModuleName = "stake"
// Name is the name of the modules.
func Name() string {
return stakingModuleName
}
//_______________________________________________________________________
// DelegatedProofOfStake - interface to enforce delegation stake
type delegatedProofOfStake interface {
declareCandidacy(TxDeclareCandidacy) error
editCandidacy(TxEditCandidacy) error
delegate(TxDelegate) error
unbond(TxUnbond) error
}
type coinSend interface {
transferFn(sender, receiver sdk.Actor, coins coin.Coins) error
}
//_______________________________________________________________________
// Handler - the transaction processing handler
type Handler struct {
}
// NewHandler returns a new Handler with the default Params
func NewHandler() Handler {
return Handler{}
}
// Name - return stake namespace
func (Handler) Name() string {
return stakingModuleName
}
// InitState - set genesis parameters for staking
func (h Handler) InitState(l log.Logger, store types.KVStore,
module, key, value string, cb sdk.InitStater) (log string, err error) {
return "", h.initState(module, key, value, store)
}
// separated for testing
func (Handler) initState(module, key, value string, store types.KVStore) error {
if module != stakingModuleName {
return sdk.ErrUnknownModule(module)
}
params := loadParams(store)
switch key {
case "allowed_bond_denom":
params.AllowedBondDenom = value
case "max_vals",
"gas_bond",
"gas_unbond":
// TODO: enforce non-negative integers in input
i, err := strconv.Atoi(value)
if err != nil {
return fmt.Errorf("input must be integer, Error: %v", err.Error())
}
switch key {
case "max_vals":
params.MaxVals = uint16(i)
case "gas_bond":
params.GasDelegate = int64(i)
case "gas_unbound":
params.GasUnbond = int64(i)
}
default:
return sdk.ErrUnknownKey(key)
}
saveParams(store, params)
return nil
}
// CheckTx checks if the tx is properly structured
func (h Handler) CheckTx(ctx sdk.Context, store types.KVStore,
tx sdk.Tx, _ sdk.Checker) (res sdk.CheckResult, err error) {
err = tx.ValidateBasic()
if err != nil {
return res, err
}
// get the sender
sender, err := getTxSender(ctx)
if err != nil {
return res, err
}
params := loadParams(store)
// create the new checker object to
checker := check{
store: store,
sender: sender,
}
// return the fee for each tx type
switch txInner := tx.Unwrap().(type) {
case TxDeclareCandidacy:
return sdk.NewCheck(params.GasDeclareCandidacy, ""),
checker.declareCandidacy(txInner)
case TxEditCandidacy:
return sdk.NewCheck(params.GasEditCandidacy, ""),
checker.editCandidacy(txInner)
case TxDelegate:
return sdk.NewCheck(params.GasDelegate, ""),
checker.delegate(txInner)
case TxUnbond:
return sdk.NewCheck(params.GasUnbond, ""),
checker.unbond(txInner)
}
return res, sdk.ErrUnknownTxType(tx)
}
// DeliverTx executes the tx if valid
func (h Handler) DeliverTx(ctx sdk.Context, store types.KVStore,
tx sdk.Tx, dispatch sdk.Deliver) (res sdk.DeliverResult, err error) {
// TODO: remove redundancy
// also we don't need to check the res - gas is already deducted in sdk
_, err = h.CheckTx(ctx, store, tx, nil)
if err != nil {
return
}
sender, err := getTxSender(ctx)
if err != nil {
return
}
params := loadParams(store)
deliverer := deliver{
store: store,
sender: sender,
params: params,
transfer: coinSender{
store: store,
dispatch: dispatch,
ctx: ctx,
}.transferFn,
}
// Run the transaction
switch _tx := tx.Unwrap().(type) {
case TxDeclareCandidacy:
res.GasUsed = params.GasDeclareCandidacy
return res, deliverer.declareCandidacy(_tx)
case TxEditCandidacy:
res.GasUsed = params.GasEditCandidacy
return res, deliverer.editCandidacy(_tx)
case TxDelegate:
res.GasUsed = params.GasDelegate
return res, deliverer.delegate(_tx)
case TxUnbond:
//context with hold account permissions
params := loadParams(store)
res.GasUsed = params.GasUnbond
ctx2 := ctx.WithPermissions(params.HoldBonded)
deliverer.transfer = coinSender{
store: store,
dispatch: dispatch,
ctx: ctx2,
}.transferFn
return res, deliverer.unbond(_tx)
}
return
}
// get the sender from the ctx and ensure it matches the tx pubkey
func getTxSender(ctx sdk.Context) (sender sdk.Actor, err error) {
senders := ctx.GetPermissions("", auth.NameSigs)
if len(senders) != 1 {
return sender, ErrMissingSignature()
}
return senders[0], nil
}
//_______________________________________________________________________
type coinSender struct {
store types.KVStore
dispatch sdk.Deliver
ctx sdk.Context
}
var _ coinSend = coinSender{} // enforce interface at compile time
func (c coinSender) transferFn(sender, receiver sdk.Actor, coins coin.Coins) error {
send := coin.NewSendOneTx(sender, receiver, coins)
// If the deduction fails (too high), abort the command
_, err := c.dispatch.DeliverTx(c.ctx, c.store, send)
return err
}
//_____________________________________________________________________
type check struct {
store types.KVStore
sender sdk.Actor
}
var _ delegatedProofOfStake = check{} // enforce interface at compile time
func (c check) declareCandidacy(tx TxDeclareCandidacy) error {
// check to see if the pubkey or sender has been registered before
candidate := loadCandidate(c.store, tx.PubKey)
if candidate != nil {
return fmt.Errorf("cannot bond to pubkey which is already declared candidacy"+
" PubKey %v already registered with %v candidate address",
candidate.PubKey, candidate.Owner)
}
return checkDenom(tx.BondUpdate, c.store)
}
func (c check) editCandidacy(tx TxEditCandidacy) error {
// candidate must already be registered
candidate := loadCandidate(c.store, tx.PubKey)
if candidate == nil { // does PubKey exist
return fmt.Errorf("cannot delegate to non-existant PubKey %v", tx.PubKey)
}
return nil
}
func (c check) delegate(tx TxDelegate) error {
candidate := loadCandidate(c.store, tx.PubKey)
if candidate == nil { // does PubKey exist
return fmt.Errorf("cannot delegate to non-existant PubKey %v", tx.PubKey)
}
return checkDenom(tx.BondUpdate, c.store)
}
func (c check) unbond(tx TxUnbond) error {
// check if bond has any shares in it unbond
bond := loadDelegatorBond(c.store, c.sender, tx.PubKey)
sharesStr := viper.GetString(tx.Shares)
if bond.Shares.LT(rational.Zero) { // bond shares < tx shares
return fmt.Errorf("no shares in account to unbond")
}
// if shares set to maximum shares then we're good
if sharesStr == "MAX" {
return nil
}
// test getting rational number from decimal provided
shares, err := rational.NewFromDecimal(sharesStr)
if err != nil {
return err
}
// test that there are enough shares to unbond
if bond.Shares.LT(shares) {
return fmt.Errorf("not enough bond shares to unbond, have %v, trying to unbond %v",
bond.Shares, tx.Shares)
}
return nil
}
func checkDenom(tx BondUpdate, store types.KVStore) error {
if tx.Bond.Denom != loadParams(store).AllowedBondDenom {
return fmt.Errorf("Invalid coin denomination")
}
return nil
}
//_____________________________________________________________________
type deliver struct {
store types.KVStore
sender sdk.Actor
params Params
gs *GlobalState
transfer transferFn
}
type transferFn func(sender, receiver sdk.Actor, coins coin.Coins) error
var _ delegatedProofOfStake = deliver{} // enforce interface at compile time
//_____________________________________________________________________
// deliver helper functions
// TODO move from deliver with new SDK should only be dependant on store to send coins in NEW SDK
// move a candidates asset pool from bonded to unbonded pool
func (d deliver) bondedToUnbondedPool(candidate *Candidate) error {
// replace bonded shares with unbonded shares
tokens := d.gs.removeSharesBonded(candidate.Assets)
candidate.Assets = d.gs.addTokensUnbonded(tokens)
candidate.Status = Unbonded
return d.transfer(d.params.HoldBonded, d.params.HoldUnbonded,
coin.Coins{{d.params.AllowedBondDenom, tokens}})
}
// move a candidates asset pool from unbonded to bonded pool
func (d deliver) unbondedToBondedPool(candidate *Candidate) error {
// replace bonded shares with unbonded shares
tokens := d.gs.removeSharesUnbonded(candidate.Assets)
candidate.Assets = d.gs.addTokensBonded(tokens)
candidate.Status = Bonded
return d.transfer(d.params.HoldUnbonded, d.params.HoldBonded,
coin.Coins{{d.params.AllowedBondDenom, tokens}})
}
//_____________________________________________________________________
// These functions assume everything has been authenticated,
// now we just perform action and save
func (d deliver) declareCandidacy(tx TxDeclareCandidacy) error {
// create and save the empty candidate
bond := loadCandidate(d.store, tx.PubKey)
if bond != nil {
return ErrCandidateExistsAddr()
}
candidate := NewCandidate(tx.PubKey, d.sender, tx.Description)
saveCandidate(d.store, candidate)
// move coins from the d.sender account to a (self-bond) delegator account
// the candidate account and global shares are updated within here
txDelegate := TxDelegate{tx.BondUpdate}
return d.delegateWithCandidate(txDelegate, candidate)
}
func (d deliver) editCandidacy(tx TxEditCandidacy) error {
// Get the pubKey bond account
candidate := loadCandidate(d.store, tx.PubKey)
if candidate == nil {
return ErrBondNotNominated()
}
if candidate.Status == Unbonded { //candidate has been withdrawn
return ErrBondNotNominated()
}
//check and edit any of the editable terms
if tx.Description.Moniker != "" {
candidate.Description.Moniker = tx.Description.Moniker
}
if tx.Description.Identity != "" {
candidate.Description.Identity = tx.Description.Identity
}
if tx.Description.Website != "" {
candidate.Description.Website = tx.Description.Website
}
if tx.Description.Details != "" {
candidate.Description.Details = tx.Description.Details
}
saveCandidate(d.store, candidate)
return nil
}
func (d deliver) delegate(tx TxDelegate) error {
// Get the pubKey bond account
candidate := loadCandidate(d.store, tx.PubKey)
if candidate == nil {
return ErrBondNotNominated()
}
return d.delegateWithCandidate(tx, candidate)
}
func (d deliver) delegateWithCandidate(tx TxDelegate, candidate *Candidate) error {
if candidate.Status == Revoked { //candidate has been withdrawn
return ErrBondNotNominated()
}
var poolAccount sdk.Actor
if candidate.Status == Bonded {
poolAccount = d.params.HoldBonded
} else {
poolAccount = d.params.HoldUnbonded
}
// TODO maybe refactor into GlobalState.addBondedTokens(), maybe with new SDK
// Move coins from the delegator account to the bonded pool account
err := d.transfer(d.sender, poolAccount, coin.Coins{tx.Bond})
if err != nil {
return err
}
// Get or create the delegator bond
bond := loadDelegatorBond(d.store, d.sender, tx.PubKey)
if bond == nil {
bond = &DelegatorBond{
PubKey: tx.PubKey,
Shares: rational.Zero,
}
}
// Account new shares, save
bond.Shares = bond.Shares.Add(candidate.addTokens(tx.Bond.Amount, d.gs))
saveCandidate(d.store, candidate)
saveDelegatorBond(d.store, d.sender, bond)
saveGlobalState(d.store, d.gs)
return nil
}
func (d deliver) unbond(tx TxUnbond) error {
// get delegator bond
bond := loadDelegatorBond(d.store, d.sender, tx.PubKey)
if bond == nil {
return ErrNoDelegatorForAddress()
}
// retrieve the amount of bonds to remove (TODO remove redundancy already serialized)
var shares rational.Rat
if tx.Shares == "MAX" {
shares = bond.Shares
} else {
var err error
shares, err = rational.NewFromDecimal(tx.Shares)
if err != nil {
return err
}
}
// subtract bond tokens from delegator bond
if bond.Shares.LT(shares) { // bond shares < tx shares
return ErrInsufficientFunds()
}
bond.Shares = bond.Shares.Sub(shares)
// get pubKey candidate
candidate := loadCandidate(d.store, tx.PubKey)
if candidate == nil {
return ErrNoCandidateForAddress()
}
revokeCandidacy := false
if bond.Shares.IsZero() {
// if the bond is the owner of the candidate then
// trigger a revoke candidacy
if d.sender.Equals(candidate.Owner) &&
candidate.Status != Revoked {
revokeCandidacy = true
}
// remove the bond
removeDelegatorBond(d.store, d.sender, tx.PubKey)
} else {
saveDelegatorBond(d.store, d.sender, bond)
}
// transfer coins back to account
var poolAccount sdk.Actor
if candidate.Status == Bonded {
poolAccount = d.params.HoldBonded
} else {
poolAccount = d.params.HoldUnbonded
}
returnCoins := candidate.removeShares(shares, d.gs)
err := d.transfer(poolAccount, d.sender,
coin.Coins{{d.params.AllowedBondDenom, returnCoins}})
if err != nil {
return err
}
// lastly if an revoke candidate if necessary
if revokeCandidacy {
// change the share types to unbonded if they were not already
if candidate.Status == Bonded {
err = d.bondedToUnbondedPool(candidate)
if err != nil {
return err
}
}
// lastly update the status
candidate.Status = Revoked
}
// deduct shares from the candidate and save
if candidate.Liabilities.IsZero() {
removeCandidate(d.store, tx.PubKey)
} else {
saveCandidate(d.store, candidate)
}
saveGlobalState(d.store, d.gs)
return nil
}

328
x/stake/handler_test.go Normal file
View File

@ -0,0 +1,328 @@
package stake
import (
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
crypto "github.com/tendermint/go-crypto"
"github.com/tendermint/tmlibs/rational"
sdk "github.com/cosmos/cosmos-sdk/types"
coin "github.com/cosmos/cosmos-sdk/x/bank" // XXX fix
)
//______________________________________________________________________
// dummy transfer functions, represents store operations on account balances
type testCoinSender struct {
store map[string]int64
}
var _ coinSend = testCoinSender{} // enforce interface at compile time
func (c testCoinSender) transferFn(sender, receiver sdk.Actor, coins coin.Coins) error {
c.store[string(sender.Address)] -= coins[0].Amount
c.store[string(receiver.Address)] += coins[0].Amount
return nil
}
//______________________________________________________________________
func initAccounts(n int, amount int64) ([]sdk.Actor, map[string]int64) {
accStore := map[string]int64{}
senders := newActors(n)
for _, sender := range senders {
accStore[string(sender.Address)] = amount
}
return senders, accStore
}
func newTxDeclareCandidacy(amt int64, pubKey crypto.PubKey) TxDeclareCandidacy {
return TxDeclareCandidacy{
BondUpdate{
PubKey: pubKey,
Bond: coin.Coin{"fermion", amt},
},
Description{},
}
}
func newTxDelegate(amt int64, pubKey crypto.PubKey) TxDelegate {
return TxDelegate{BondUpdate{
PubKey: pubKey,
Bond: coin.Coin{"fermion", amt},
}}
}
func newTxUnbond(shares string, pubKey crypto.PubKey) TxUnbond {
return TxUnbond{
PubKey: pubKey,
Shares: shares,
}
}
func paramsNoInflation() Params {
return Params{
HoldBonded: sdk.NewActor(stakingModuleName, []byte("77777777777777777777777777777777")),
HoldUnbonded: sdk.NewActor(stakingModuleName, []byte("88888888888888888888888888888888")),
InflationRateChange: rational.Zero,
InflationMax: rational.Zero,
InflationMin: rational.Zero,
GoalBonded: rational.New(67, 100),
MaxVals: 100,
AllowedBondDenom: "fermion",
GasDeclareCandidacy: 20,
GasEditCandidacy: 20,
GasDelegate: 20,
GasUnbond: 20,
}
}
func newDeliver(t, sender sdk.Actor, accStore map[string]int64) deliver {
store := initTestStore()
params := paramsNoInflation()
saveParams(store, params)
return deliver{
store: store,
sender: sender,
params: params,
gs: loadGlobalState(store),
transfer: testCoinSender{accStore}.transferFn,
}
}
func TestDuplicatesTxDeclareCandidacy(t *testing.T) {
senders, accStore := initAccounts(2, 1000) // for accounts
deliverer := newDeliver(t, senders[0], accStore)
checker := check{
store: deliverer.store,
sender: senders[0],
}
txDeclareCandidacy := newTxDeclareCandidacy(10, pks[0])
got := deliverer.declareCandidacy(txDeclareCandidacy)
assert.NoError(t, got, "expected no error on runTxDeclareCandidacy")
// one sender can bond to two different pubKeys
txDeclareCandidacy.PubKey = pks[1]
err := checker.declareCandidacy(txDeclareCandidacy)
assert.Nil(t, err, "didn't expected error on checkTx")
// two senders cant bond to the same pubkey
checker.sender = senders[1]
txDeclareCandidacy.PubKey = pks[0]
err = checker.declareCandidacy(txDeclareCandidacy)
assert.NotNil(t, err, "expected error on checkTx")
}
func TestIncrementsTxDelegate(t *testing.T) {
initSender := int64(1000)
senders, accStore := initAccounts(1, initSender) // for accounts
deliverer := newDeliver(t, senders[0], accStore)
// first declare candidacy
bondAmount := int64(10)
txDeclareCandidacy := newTxDeclareCandidacy(bondAmount, pks[0])
got := deliverer.declareCandidacy(txDeclareCandidacy)
assert.NoError(t, got, "expected declare candidacy tx to be ok, got %v", got)
expectedBond := bondAmount // 1 since we send 1 at the start of loop,
// just send the same txbond multiple times
holder := deliverer.params.HoldUnbonded // XXX this should be HoldBonded, new SDK updates
txDelegate := newTxDelegate(bondAmount, pks[0])
for i := 0; i < 5; i++ {
got := deliverer.delegate(txDelegate)
assert.NoError(t, got, "expected tx %d to be ok, got %v", i, got)
//Check that the accounts and the bond account have the appropriate values
candidates := loadCandidates(deliverer.store)
expectedBond += bondAmount
expectedSender := initSender - expectedBond
gotBonded := candidates[0].Liabilities.Evaluate()
gotHolder := accStore[string(holder.Address)]
gotSender := accStore[string(deliverer.sender.Address)]
assert.Equal(t, expectedBond, gotBonded, "i: %v, %v, %v", i, expectedBond, gotBonded)
assert.Equal(t, expectedBond, gotHolder, "i: %v, %v, %v", i, expectedBond, gotHolder)
assert.Equal(t, expectedSender, gotSender, "i: %v, %v, %v", i, expectedSender, gotSender)
}
}
func TestIncrementsTxUnbond(t *testing.T) {
initSender := int64(0)
senders, accStore := initAccounts(1, initSender) // for accounts
deliverer := newDeliver(t, senders[0], accStore)
// set initial bond
initBond := int64(1000)
accStore[string(deliverer.sender.Address)] = initBond
got := deliverer.declareCandidacy(newTxDeclareCandidacy(initBond, pks[0]))
assert.NoError(t, got, "expected initial bond tx to be ok, got %v", got)
// just send the same txunbond multiple times
holder := deliverer.params.HoldUnbonded // XXX new SDK, this should be HoldBonded
// XXX use decimals here
unbondShares, unbondSharesStr := int64(10), "10"
txUndelegate := newTxUnbond(unbondSharesStr, pks[0])
nUnbonds := 5
for i := 0; i < nUnbonds; i++ {
got := deliverer.unbond(txUndelegate)
assert.NoError(t, got, "expected tx %d to be ok, got %v", i, got)
//Check that the accounts and the bond account have the appropriate values
candidates := loadCandidates(deliverer.store)
expectedBond := initBond - int64(i+1)*unbondShares // +1 since we send 1 at the start of loop
expectedSender := initSender + (initBond - expectedBond)
gotBonded := candidates[0].Liabilities.Evaluate()
gotHolder := accStore[string(holder.Address)]
gotSender := accStore[string(deliverer.sender.Address)]
assert.Equal(t, expectedBond, gotBonded, "%v, %v", expectedBond, gotBonded)
assert.Equal(t, expectedBond, gotHolder, "%v, %v", expectedBond, gotHolder)
assert.Equal(t, expectedSender, gotSender, "%v, %v", expectedSender, gotSender)
}
// these are more than we have bonded now
errorCases := []int64{
//1<<64 - 1, // more than int64
//1<<63 + 1, // more than int64
1<<63 - 1,
1 << 31,
initBond,
}
for _, c := range errorCases {
unbondShares := strconv.Itoa(int(c))
txUndelegate := newTxUnbond(unbondShares, pks[0])
got = deliverer.unbond(txUndelegate)
assert.Error(t, got, "expected unbond tx to fail")
}
leftBonded := initBond - unbondShares*int64(nUnbonds)
// should be unable to unbond one more than we have
txUndelegate = newTxUnbond(strconv.Itoa(int(leftBonded)+1), pks[0])
got = deliverer.unbond(txUndelegate)
assert.Error(t, got, "expected unbond tx to fail")
// should be able to unbond just what we have
txUndelegate = newTxUnbond(strconv.Itoa(int(leftBonded)), pks[0])
got = deliverer.unbond(txUndelegate)
assert.NoError(t, got, "expected unbond tx to pass")
}
func TestMultipleTxDeclareCandidacy(t *testing.T) {
initSender := int64(1000)
senders, accStore := initAccounts(3, initSender)
pubKeys := []crypto.PubKey{pks[0], pks[1], pks[2]}
deliverer := newDeliver(t, senders[0], accStore)
// bond them all
for i, sender := range senders {
txDeclareCandidacy := newTxDeclareCandidacy(10, pubKeys[i])
deliverer.sender = sender
got := deliverer.declareCandidacy(txDeclareCandidacy)
assert.NoError(t, got, "expected tx %d to be ok, got %v", i, got)
//Check that the account is bonded
candidates := loadCandidates(deliverer.store)
val := candidates[i]
balanceGot, balanceExpd := accStore[string(val.Owner.Address)], initSender-10
assert.Equal(t, i+1, len(candidates), "expected %d candidates got %d, candidates: %v", i+1, len(candidates), candidates)
assert.Equal(t, 10, int(val.Liabilities.Evaluate()), "expected %d shares, got %d", 10, val.Liabilities)
assert.Equal(t, balanceExpd, balanceGot, "expected account to have %d, got %d", balanceExpd, balanceGot)
}
// unbond them all
for i, sender := range senders {
candidatePre := loadCandidate(deliverer.store, pubKeys[i])
txUndelegate := newTxUnbond("10", pubKeys[i])
deliverer.sender = sender
got := deliverer.unbond(txUndelegate)
assert.NoError(t, got, "expected tx %d to be ok, got %v", i, got)
//Check that the account is unbonded
candidates := loadCandidates(deliverer.store)
assert.Equal(t, len(senders)-(i+1), len(candidates), "expected %d candidates got %d", len(senders)-(i+1), len(candidates))
candidatePost := loadCandidate(deliverer.store, pubKeys[i])
balanceGot, balanceExpd := accStore[string(candidatePre.Owner.Address)], initSender
assert.Nil(t, candidatePost, "expected nil candidate retrieve, got %d", 0, candidatePost)
assert.Equal(t, balanceExpd, balanceGot, "expected account to have %d, got %d", balanceExpd, balanceGot)
}
}
func TestMultipleTxDelegate(t *testing.T) {
accounts, accStore := initAccounts(3, 1000)
sender, delegators := accounts[0], accounts[1:]
deliverer := newDeliver(t, sender, accStore)
//first make a candidate
txDeclareCandidacy := newTxDeclareCandidacy(10, pks[0])
got := deliverer.declareCandidacy(txDeclareCandidacy)
require.NoError(t, got, "expected tx to be ok, got %v", got)
// delegate multiple parties
for i, delegator := range delegators {
txDelegate := newTxDelegate(10, pks[0])
deliverer.sender = delegator
got := deliverer.delegate(txDelegate)
require.NoError(t, got, "expected tx %d to be ok, got %v", i, got)
//Check that the account is bonded
bond := loadDelegatorBond(deliverer.store, delegator, pks[0])
assert.NotNil(t, bond, "expected delegatee bond %d to exist", bond)
}
// unbond them all
for i, delegator := range delegators {
txUndelegate := newTxUnbond("10", pks[0])
deliverer.sender = delegator
got := deliverer.unbond(txUndelegate)
require.NoError(t, got, "expected tx %d to be ok, got %v", i, got)
//Check that the account is unbonded
bond := loadDelegatorBond(deliverer.store, delegator, pks[0])
assert.Nil(t, bond, "expected delegatee bond %d to be nil", bond)
}
}
func TestVoidCandidacy(t *testing.T) {
accounts, accStore := initAccounts(2, 1000) // for accounts
sender, delegator := accounts[0], accounts[1]
deliverer := newDeliver(t, sender, accStore)
// create the candidate
txDeclareCandidacy := newTxDeclareCandidacy(10, pks[0])
got := deliverer.declareCandidacy(txDeclareCandidacy)
require.NoError(t, got, "expected no error on runTxDeclareCandidacy")
// bond a delegator
txDelegate := newTxDelegate(10, pks[0])
deliverer.sender = delegator
got = deliverer.delegate(txDelegate)
require.NoError(t, got, "expected ok, got %v", got)
// unbond the candidates bond portion
txUndelegate := newTxUnbond("10", pks[0])
deliverer.sender = sender
got = deliverer.unbond(txUndelegate)
require.NoError(t, got, "expected no error on runTxDeclareCandidacy")
// test that this pubkey cannot yet be bonded too
deliverer.sender = delegator
got = deliverer.delegate(txDelegate)
assert.Error(t, got, "expected error, got %v", got)
// test that the delegator can still withdraw their bonds
got = deliverer.unbond(txUndelegate)
require.NoError(t, got, "expected no error on runTxDeclareCandidacy")
// verify that the pubkey can now be reused
got = deliverer.declareCandidacy(txDeclareCandidacy)
assert.NoError(t, got, "expected ok, got %v", got)
}

74
x/stake/tick.go Normal file
View File

@ -0,0 +1,74 @@
package stake
import (
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/abci/types"
"github.com/tendermint/tmlibs/rational"
)
// Tick - called at the end of every block
func Tick(ctx sdk.Context, store types.KVStore) (change []*abci.Validator, err error) {
// retrieve params
params := loadParams(store)
gs := loadGlobalState(store)
height := ctx.BlockHeight()
// Process Validator Provisions
// XXX right now just process every 5 blocks, in new SDK make hourly
if gs.InflationLastTime+5 <= height {
gs.InflationLastTime = height
processProvisions(store, gs, params)
}
return UpdateValidatorSet(store, gs, params)
}
var hrsPerYr = rational.New(8766) // as defined by a julian year of 365.25 days
// process provisions for an hour period
func processProvisions(store types.KVStore, gs *GlobalState, params Params) {
gs.Inflation = nextInflation(gs, params).Round(1000000000)
// Because the validators hold a relative bonded share (`GlobalStakeShare`), when
// more bonded tokens are added proportionally to all validators the only term
// which needs to be updated is the `BondedPool`. So for each previsions cycle:
provisions := gs.Inflation.Mul(rational.New(gs.TotalSupply)).Quo(hrsPerYr).Evaluate()
gs.BondedPool += provisions
gs.TotalSupply += provisions
// XXX XXX XXX XXX XXX XXX XXX XXX XXX
// XXX Mint them to the hold account
// XXX XXX XXX XXX XXX XXX XXX XXX XXX
// save the params
saveGlobalState(store, gs)
}
// get the next inflation rate for the hour
func nextInflation(gs *GlobalState, params Params) (inflation rational.Rat) {
// The target annual inflation rate is recalculated for each previsions cycle. The
// inflation is also subject to a rate change (positive of negative) depending or
// the distance from the desired ratio (67%). The maximum rate change possible is
// defined to be 13% per year, however the annual inflation is capped as between
// 7% and 20%.
// (1 - bondedRatio/GoalBonded) * InflationRateChange
inflationRateChangePerYear := rational.One.Sub(gs.bondedRatio().Quo(params.GoalBonded)).Mul(params.InflationRateChange)
inflationRateChange := inflationRateChangePerYear.Quo(hrsPerYr)
// increase the new annual inflation for this next cycle
inflation = gs.Inflation.Add(inflationRateChange)
if inflation.GT(params.InflationMax) {
inflation = params.InflationMax
}
if inflation.LT(params.InflationMin) {
inflation = params.InflationMin
}
return
}

116
x/stake/tick_test.go Normal file
View File

@ -0,0 +1,116 @@
package stake
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/tendermint/tmlibs/rational"
)
func TestGetInflation(t *testing.T) {
store := initTestStore(t)
params := loadParams(store)
gs := loadGlobalState(store)
// Governing Mechanism:
// bondedRatio = BondedPool / TotalSupply
// inflationRateChangePerYear = (1- bondedRatio/ GoalBonded) * MaxInflationRateChange
tests := []struct {
setBondedPool, setTotalSupply int64
setInflation, expectedChange rational.Rat
}{
// with 0% bonded atom supply the inflation should increase by InflationRateChange
{0, 0, rational.New(7, 100), params.InflationRateChange.Quo(hrsPerYr)},
// 100% bonded, starting at 20% inflation and being reduced
{1, 1, rational.New(20, 100), rational.One.Sub(rational.One.Quo(params.GoalBonded)).Mul(params.InflationRateChange).Quo(hrsPerYr)},
// 50% bonded, starting at 10% inflation and being increased
{1, 2, rational.New(10, 100), rational.One.Sub(rational.New(1, 2).Quo(params.GoalBonded)).Mul(params.InflationRateChange).Quo(hrsPerYr)},
// test 7% minimum stop (testing with 100% bonded)
{1, 1, rational.New(7, 100), rational.Zero},
{1, 1, rational.New(70001, 1000000), rational.New(-1, 1000000)},
// test 20% maximum stop (testing with 0% bonded)
{0, 0, rational.New(20, 100), rational.Zero},
{0, 0, rational.New(199999, 1000000), rational.New(1, 1000000)},
// perfect balance shouldn't change inflation
{67, 100, rational.New(15, 100), rational.Zero},
}
for _, tc := range tests {
gs.BondedPool, gs.TotalSupply = tc.setBondedPool, tc.setTotalSupply
gs.Inflation = tc.setInflation
inflation := nextInflation(gs, params)
diffInflation := inflation.Sub(tc.setInflation)
assert.True(t, diffInflation.Equal(tc.expectedChange),
"%v, %v", diffInflation, tc.expectedChange)
}
}
func TestProcessProvisions(t *testing.T) {
store := initTestStore(t)
params := loadParams(store)
gs := loadGlobalState(store)
// create some candidates some bonded, some unbonded
n := 10
actors := newActors(n)
candidates := candidatesFromActorsEmpty(actors)
for i, candidate := range candidates {
if i < 5 {
candidate.Status = Bonded
}
mintedTokens := int64((i + 1) * 10000000)
gs.TotalSupply += mintedTokens
candidate.addTokens(mintedTokens, gs)
saveCandidate(store, candidate)
}
var totalSupply int64 = 550000000
var bondedShares int64 = 150000000
var unbondedShares int64 = 400000000
// initial bonded ratio ~ 27%
assert.True(t, gs.bondedRatio().Equal(rational.New(bondedShares, totalSupply)), "%v", gs.bondedRatio())
// Supplies
assert.Equal(t, totalSupply, gs.TotalSupply)
assert.Equal(t, bondedShares, gs.BondedPool)
assert.Equal(t, unbondedShares, gs.UnbondedPool)
// test the value of candidate shares
assert.True(t, gs.bondedShareExRate().Equal(rational.One), "%v", gs.bondedShareExRate())
initialSupply := gs.TotalSupply
initialUnbonded := gs.TotalSupply - gs.BondedPool
// process the provisions a year
for hr := 0; hr < 8766; hr++ {
expInflation := nextInflation(gs, params).Round(1000000000)
expProvisions := (expInflation.Mul(rational.New(gs.TotalSupply)).Quo(hrsPerYr)).Evaluate()
startBondedPool := gs.BondedPool
startTotalSupply := gs.TotalSupply
processProvisions(store, gs, params)
assert.Equal(t, startBondedPool+expProvisions, gs.BondedPool)
assert.Equal(t, startTotalSupply+expProvisions, gs.TotalSupply)
}
assert.NotEqual(t, initialSupply, gs.TotalSupply)
assert.Equal(t, initialUnbonded, gs.UnbondedPool)
//panic(fmt.Sprintf("debug total %v, bonded %v, diff %v\n", gs.TotalSupply, gs.BondedPool, gs.TotalSupply-gs.BondedPool))
// initial bonded ratio ~ 35% ~ 30% increase for bonded holders
assert.True(t, gs.bondedRatio().Equal(rational.New(105906511, 305906511)), "%v", gs.bondedRatio())
// global supply
assert.Equal(t, int64(611813022), gs.TotalSupply)
assert.Equal(t, int64(211813022), gs.BondedPool)
assert.Equal(t, unbondedShares, gs.UnbondedPool)
// test the value of candidate shares
assert.True(t, gs.bondedShareExRate().Mul(rational.New(bondedShares)).Equal(rational.New(211813022)), "%v", gs.bondedShareExRate())
}