From 266a8392d3e643fb33d84567c217c7a5146c0e83 Mon Sep 17 00:00:00 2001 From: rigelrozanski Date: Fri, 12 Jan 2018 05:30:39 +0000 Subject: [PATCH] initial add gaia working --- examples/gaia/client.go | 97 ++++++ examples/gaia/main.go | 50 +++ examples/gaia/node.go | 69 +++++ examples/gaia/rest.go | 89 ++++++ examples/gaia/sh_tests/stake.sh | 275 +++++++++++++++++ x/stake/commands/query.go | 136 ++++++++ x/stake/commands/tx.go | 195 ++++++++++++ x/stake/errors.go | 53 ++++ x/stake/handler.go | 529 ++++++++++++++++++++++++++++++++ x/stake/handler_test.go | 336 ++++++++++++++++++++ x/stake/rest/query.go | 188 ++++++++++++ x/stake/rest/tx.go | 159 ++++++++++ x/stake/state.go | 273 ++++++++++++++++ x/stake/state_test.go | 116 +++++++ x/stake/test_common.go | 91 ++++++ x/stake/tick.go | 75 +++++ x/stake/tick_test.go | 120 ++++++++ x/stake/tx.go | 147 +++++++++ x/stake/tx_test.go | 104 +++++++ x/stake/types.go | 414 +++++++++++++++++++++++++ x/stake/types_test.go | 251 +++++++++++++++ 21 files changed, 3767 insertions(+) create mode 100644 examples/gaia/client.go create mode 100644 examples/gaia/main.go create mode 100644 examples/gaia/node.go create mode 100644 examples/gaia/rest.go create mode 100644 examples/gaia/sh_tests/stake.sh create mode 100644 x/stake/commands/query.go create mode 100644 x/stake/commands/tx.go create mode 100644 x/stake/errors.go create mode 100644 x/stake/handler.go create mode 100644 x/stake/handler_test.go create mode 100644 x/stake/rest/query.go create mode 100644 x/stake/rest/tx.go create mode 100644 x/stake/state.go create mode 100644 x/stake/state_test.go create mode 100644 x/stake/test_common.go create mode 100644 x/stake/tick.go create mode 100644 x/stake/tick_test.go create mode 100644 x/stake/tx.go create mode 100644 x/stake/tx_test.go create mode 100644 x/stake/types.go create mode 100644 x/stake/types_test.go diff --git a/examples/gaia/client.go b/examples/gaia/client.go new file mode 100644 index 0000000000..6344fc3489 --- /dev/null +++ b/examples/gaia/client.go @@ -0,0 +1,97 @@ +package main + +import ( + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client/commands" + "github.com/cosmos/cosmos-sdk/client/commands/commits" + "github.com/cosmos/cosmos-sdk/client/commands/keys" + "github.com/cosmos/cosmos-sdk/client/commands/proxy" + "github.com/cosmos/cosmos-sdk/client/commands/query" + rpccmd "github.com/cosmos/cosmos-sdk/client/commands/rpc" + txcmd "github.com/cosmos/cosmos-sdk/client/commands/txs" + authcmd "github.com/cosmos/cosmos-sdk/modules/auth/commands" + basecmd "github.com/cosmos/cosmos-sdk/modules/base/commands" + coincmd "github.com/cosmos/cosmos-sdk/modules/coin/commands" + feecmd "github.com/cosmos/cosmos-sdk/modules/fee/commands" + ibccmd "github.com/cosmos/cosmos-sdk/modules/ibc/commands" + noncecmd "github.com/cosmos/cosmos-sdk/modules/nonce/commands" + rolecmd "github.com/cosmos/cosmos-sdk/modules/roles/commands" + + stakecmd "github.com/cosmos/gaia/modules/stake/commands" +) + +// clientCmd is the entry point for this binary +var clientCmd = &cobra.Command{ + Use: "client", + Short: "Gaia light client", + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, +} + +func prepareClientCommands() { + commands.AddBasicFlags(clientCmd) + + // Prepare queries + query.RootCmd.AddCommand( + // These are default parsers, but optional in your app (you can remove key) + query.TxQueryCmd, + query.KeyQueryCmd, + coincmd.AccountQueryCmd, + noncecmd.NonceQueryCmd, + rolecmd.RoleQueryCmd, + ibccmd.IBCQueryCmd, + + //stakecmd.CmdQueryValidator, + stakecmd.CmdQueryCandidates, + stakecmd.CmdQueryCandidate, + stakecmd.CmdQueryDelegatorBond, + stakecmd.CmdQueryDelegatorCandidates, + ) + + // set up the middleware + txcmd.Middleware = txcmd.Wrappers{ + feecmd.FeeWrapper{}, + rolecmd.RoleWrapper{}, + noncecmd.NonceWrapper{}, + basecmd.ChainWrapper{}, + authcmd.SigWrapper{}, + } + txcmd.Middleware.Register(txcmd.RootCmd.PersistentFlags()) + + // you will always want this for the base send command + txcmd.RootCmd.AddCommand( + // This is the default transaction, optional in your app + coincmd.SendTxCmd, + coincmd.CreditTxCmd, + // this enables creating roles + rolecmd.CreateRoleTxCmd, + // these are for handling ibc + ibccmd.RegisterChainTxCmd, + ibccmd.UpdateChainTxCmd, + ibccmd.PostPacketTxCmd, + + stakecmd.CmdDeclareCandidacy, + stakecmd.CmdEditCandidacy, + stakecmd.CmdDelegate, + stakecmd.CmdUnbond, + ) + + clientCmd.AddCommand( + proxy.RootCmd, + lineBreak, + + txcmd.RootCmd, + query.RootCmd, + rpccmd.RootCmd, + lineBreak, + + keys.RootCmd, + commands.InitCmd, + commands.ResetCmd, + commits.RootCmd, + lineBreak, + ) + +} diff --git a/examples/gaia/main.go b/examples/gaia/main.go new file mode 100644 index 0000000000..b631e5cba3 --- /dev/null +++ b/examples/gaia/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "os" + + "github.com/spf13/cobra" + + "github.com/tendermint/tmlibs/cli" + + basecmd "github.com/cosmos/cosmos-sdk/server/commands" + "github.com/cosmos/gaia/version" +) + +// GaiaCmd is the entry point for this binary +var ( + GaiaCmd = &cobra.Command{ + Use: "gaia", + Short: "The Cosmos Network delegation-game test", + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, + } + + lineBreak = &cobra.Command{Run: func(*cobra.Command, []string) {}} +) + +func main() { + // disable sorting + cobra.EnableCommandSorting = false + + // add commands + prepareNodeCommands() + prepareRestServerCommands() + prepareClientCommands() + + GaiaCmd.AddCommand( + nodeCmd, + restServerCmd, + clientCmd, + + lineBreak, + version.VersionCmd, + //auto.AutoCompleteCmd, + ) + + // prepare and add flags + basecmd.SetUpRoot(GaiaCmd) + executor := cli.PrepareMainCmd(GaiaCmd, "GA", os.ExpandEnv("$HOME/.cosmos-gaia-cli")) + executor.Execute() +} diff --git a/examples/gaia/node.go b/examples/gaia/node.go new file mode 100644 index 0000000000..aec74aeaf0 --- /dev/null +++ b/examples/gaia/node.go @@ -0,0 +1,69 @@ +package main + +import ( + "github.com/spf13/cobra" + + abci "github.com/tendermint/abci/types" + + sdk "github.com/cosmos/cosmos-sdk" + "github.com/cosmos/cosmos-sdk/modules/auth" + "github.com/cosmos/cosmos-sdk/modules/base" + "github.com/cosmos/cosmos-sdk/modules/coin" + "github.com/cosmos/cosmos-sdk/modules/fee" + "github.com/cosmos/cosmos-sdk/modules/ibc" + "github.com/cosmos/cosmos-sdk/modules/nonce" + "github.com/cosmos/cosmos-sdk/modules/roles" + basecmd "github.com/cosmos/cosmos-sdk/server/commands" + "github.com/cosmos/cosmos-sdk/stack" + "github.com/cosmos/cosmos-sdk/state" + + "github.com/cosmos/gaia/modules/stake" +) + +// nodeCmd is the entry point for this binary +var nodeCmd = &cobra.Command{ + Use: "node", + Short: "The Cosmos Network delegation-game blockchain test", + Run: func(cmd *cobra.Command, args []string) { cmd.Help() }, +} + +func prepareNodeCommands() { + + basecmd.Handler = stack.New( + base.Logger{}, + stack.Recovery{}, + auth.Signatures{}, + base.Chain{}, + stack.Checkpoint{OnCheck: true}, + nonce.ReplayCheck{}, + ). + IBC(ibc.NewMiddleware()). + Apps( + roles.NewMiddleware(), + fee.NewSimpleFeeMiddleware(coin.Coin{"fermion", 0}, fee.Bank), + stack.Checkpoint{OnDeliver: true}, + ). + Dispatch( + coin.NewHandler(), + stack.WrapHandler(roles.NewHandler()), + stack.WrapHandler(ibc.NewHandler()), + stake.NewHandler(), + ) + + nodeCmd.AddCommand( + basecmd.GetInitCmd("fermion", []string{"stake/allowed_bond_denom/fermion"}), + basecmd.GetTickStartCmd(sdk.TickerFunc(tickFn)), + basecmd.UnsafeResetAllCmd, + ) +} + +// Tick - Called every block even if no transaction, process all queues, +// validator rewards, and calculate the validator set difference +func tickFn(ctx sdk.Context, store state.SimpleDB) (change []*abci.Validator, err error) { + // first need to prefix the store, at this point it's a global store + store = stack.PrefixedStore(stake.Name(), store) + + // execute Tick + change, err = stake.Tick(ctx, store) + return +} diff --git a/examples/gaia/rest.go b/examples/gaia/rest.go new file mode 100644 index 0000000000..7a96978961 --- /dev/null +++ b/examples/gaia/rest.go @@ -0,0 +1,89 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + "github.com/gorilla/mux" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/tendermint/tmlibs/cli" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/commands" + rest "github.com/cosmos/cosmos-sdk/client/rest" + coinrest "github.com/cosmos/cosmos-sdk/modules/coin/rest" + noncerest "github.com/cosmos/cosmos-sdk/modules/nonce/rest" + rolerest "github.com/cosmos/cosmos-sdk/modules/roles/rest" + + stakerest "github.com/cosmos/gaia/modules/stake/rest" +) + +const defaultAlgo = "ed25519" + +var ( + restServerCmd = &cobra.Command{ + Use: "rest-server", + Short: "REST client for gaia commands", + Long: `Gaiaserver presents a nice (not raw hex) interface to the gaia blockchain structure.`, + RunE: func(cmd *cobra.Command, args []string) error { + return cmdRestServer(cmd, args) + }, + } + + flagPort = "port" +) + +func prepareRestServerCommands() { + commands.AddBasicFlags(restServerCmd) + restServerCmd.PersistentFlags().IntP(flagPort, "p", 8998, "port to run the server on") +} + +func cmdRestServer(cmd *cobra.Command, args []string) error { + router := mux.NewRouter() + + rootDir := viper.GetString(cli.HomeFlag) + keyMan := client.GetKeyManager(rootDir) + serviceKeys := rest.NewServiceKeys(keyMan) + serviceTxs := rest.NewServiceTxs(commands.GetNode()) + + routeRegistrars := []func(*mux.Router) error{ + // rest.Keys handlers + serviceKeys.RegisterCRUD, + + // Coin handlers (Send, Query, SearchSent) + coinrest.RegisterAll, + + // Roles createRole handler + rolerest.RegisterCreateRole, + + // Gaia sign transactions handler + serviceKeys.RegisterSignTx, + // Gaia post transaction handler + serviceTxs.RegisterPostTx, + + // Nonce query handler + noncerest.RegisterQueryNonce, + + // Staking query handlers + stakerest.RegisterQueryCandidate, + stakerest.RegisterQueryCandidates, + stakerest.RegisterQueryDelegatorBond, + stakerest.RegisterQueryDelegatorCandidates, + // Staking tx builders + stakerest.RegisterDelegate, + stakerest.RegisterUnbond, + } + + for _, routeRegistrar := range routeRegistrars { + if err := routeRegistrar(router); err != nil { + log.Fatal(err) + } + } + + addr := fmt.Sprintf(":%d", viper.GetInt(flagPort)) + + log.Printf("Serving on %q", addr) + return http.ListenAndServe(addr, router) +} diff --git a/examples/gaia/sh_tests/stake.sh b/examples/gaia/sh_tests/stake.sh new file mode 100644 index 0000000000..4f5d1c7703 --- /dev/null +++ b/examples/gaia/sh_tests/stake.sh @@ -0,0 +1,275 @@ +#!/bin/bash +set -u + +# These global variables are required for common.sh +SERVER_EXE="gaia node" +CLIENT_EXE="gaia client" +ACCOUNTS=(jae ethan bucky rigel igor) +RICH=${ACCOUNTS[0]} +DELEGATOR=${ACCOUNTS[2]} +POOR=${ACCOUNTS[4]} + +BASE_DIR=$HOME/stake_test +BASE_DIR2=$HOME/stake_test2 +SERVER1=$BASE_DIR/server +SERVER2=$BASE_DIR2/server + +oneTimeSetUp() { + #[ "$2" ] || echo "missing parameters, line=${LINENO}" ; exit 1; + + + # These are passed in as args + CHAIN_ID="stake_test" + + # TODO Make this more robust + if [ "$BASE_DIR" == "$HOME/" ]; then + echo "Must be called with argument, or it will wipe your home directory" + exit 1 + fi + + rm -rf $BASE_DIR 2>/dev/null + mkdir -p $BASE_DIR + + if [ "$BASE_DIR2" == "$HOME/" ]; then + echo "Must be called with argument, or it will wipe your home directory" + exit 1 + fi + rm -rf $BASE_DIR2 2>/dev/null + mkdir -p $BASE_DIR2 + + # Set up client - make sure you use the proper prefix if you set + # a custom CLIENT_EXE + export BC_HOME=${BASE_DIR}/client + prepareClient + + # start the node server + set +u ; initServer $BASE_DIR $CHAIN_ID ; set -u + if [ $? != 0 ]; then return 1; fi + + set +u ; initClient $CHAIN_ID ; set -u + if [ $? != 0 ]; then return 1; fi + + printf "...Testing may begin!\n\n\n" + +} + +oneTimeTearDown() { + kill -9 $PID_SERVER2 >/dev/null 2>&1 + set +u ; quickTearDown ; set -u +} + +# Ex Usage: checkCandidate $PUBKEY $EXPECTED_VOTING_POWER +checkCandidate() { + CANDIDATE=$(${CLIENT_EXE} query candidate --pubkey=$1) + if ! assertTrue "line=${LINENO}, bad query" $?; then + return 1 + fi + assertEquals "line=${LINENO}, proper voting power" "$2" $(echo $CANDIDATE | jq .data.voting_power) + return $? +} + +# Ex Usage: checkCandidate $PUBKEY +checkCandidateEmpty() { + CANDIDATE=$(${CLIENT_EXE} query candidate --pubkey=$1 2>/dev/null) + if ! assertFalse "line=${LINENO}, expected empty query" $?; then + return 1 + fi +} + +# Ex Usage: checkCandidate $DELEGATOR_ADDR $PUBKEY $EXPECTED_SHARES +checkDelegatorBond() { + BOND=$(${CLIENT_EXE} query delegator-bond --delegator-address=$1 --pubkey=$2) + if ! assertTrue "line=${LINENO}, account must exist" $?; then + return 1 + fi + assertEquals "line=${LINENO}, proper bond amount" "$3" $(echo $BOND | jq .data.Shares) + return $? +} + +# Ex Usage: checkCandidate $DELEGATOR_ADDR $PUBKEY +checkDelegatorBondEmpty() { + BOND=$(${CLIENT_EXE} query delegator-bond --delegator-address=$1 --pubkey=$2 2>/dev/null) + if ! assertFalse "line=${LINENO}, expected empty query" $?; then + return 1 + fi +} + +#______________________________________________________________________________________ + +test00GetAccount() { + SENDER=$(getAddr $RICH) + RECV=$(getAddr $POOR) + + assertFalse "line=${LINENO}, requires arg" "${CLIENT_EXE} query account" + + set +u ; checkAccount $SENDER "9007199254740992" ; set -u + + ACCT2=$(${CLIENT_EXE} query account $RECV 2>/dev/null) + assertFalse "line=${LINENO}, has no genesis account" $? +} + +test01SendTx() { + assertFalse "line=${LINENO}, missing dest" "${CLIENT_EXE} tx send --amount=992fermion --sequence=1" + assertFalse "line=${LINENO}, bad password" "echo foo | ${CLIENT_EXE} tx send --amount=992fermion --sequence=1 --to=$RECV --name=$RICH" + TX=$(echo qwertyuiop | ${CLIENT_EXE} tx send --amount=992fermion --sequence=1 --to=$RECV --name=$RICH) + txSucceeded $? "$TX" "$RECV" + HASH=$(echo $TX | jq .hash | tr -d \") + TX_HEIGHT=$(echo $TX | jq .height) + + set +u + checkAccount $SENDER "9007199254740000" $TX_HEIGHT + # make sure 0x prefix also works + checkAccount "0x$SENDER" "9007199254740000" $TX_HEIGHT + checkAccount $RECV "992" $TX_HEIGHT + + # Make sure tx is indexed + checkSendTx $HASH $TX_HEIGHT $SENDER "992" + set -u +} + +test02DeclareCandidacy() { + + # the premise of this test is to run a second validator (from rich) and then bond and unbond some tokens + # first create a second node to run and connect to the system + + # init the second node + SERVER_LOG2=$BASE_DIR2/node2.log + GENKEY=$(${CLIENT_EXE} keys get ${RICH} | awk '{print $2}') + ${SERVER_EXE} init $GENKEY --chain-id $CHAIN_ID --home=$SERVER2 >>$SERVER_LOG2 + if [ $? != 0 ]; then return 1; fi + + # copy in the genesis from the first initialization to the new server + cp $SERVER1/genesis.json $SERVER2/genesis.json + + # point the new config to the old server location + rm $SERVER2/config.toml + echo 'proxy_app = "tcp://127.0.0.1:46668" + moniker = "anonymous" + fast_sync = true + db_backend = "leveldb" + log_level = "state:info,*:error" + + [rpc] + laddr = "tcp://0.0.0.0:46667" + + [p2p] + laddr = "tcp://0.0.0.0:46666" + seeds = "0.0.0.0:46656"' >$SERVER2/config.toml + + # start the second node + ${SERVER_EXE} start --home=$SERVER2 >>$SERVER_LOG2 2>&1 & + sleep 1 + PID_SERVER2=$! + disown + if ! ps $PID_SERVER2 >/dev/null; then + echo "**FAILED**" + cat $SERVER_LOG2 + return 1 + fi + + # get the pubkey of the second validator + PK2=$(cat $SERVER2/priv_validator.json | jq -r .pub_key.data) + + CAND_ADDR=$(getAddr $POOR) + TX=$(echo qwertyuiop | ${CLIENT_EXE} tx declare-candidacy --sequence=1 --amount=10fermion --name=$POOR --pubkey=$PK2 --moniker=rigey) + if [ $? != 0 ]; then return 1; fi + HASH=$(echo $TX | jq .hash | tr -d \") + TX_HEIGHT=$(echo $TX | jq .height) + set +u ; checkAccount $CAND_ADDR "982" $TX_HEIGHT ; set -u + checkCandidate $PK2 "10" + checkDelegatorBond $CAND_ADDR $PK2 "10" +} + +test03Delegate() { + # send some coins to a delegator + DELA_ADDR=$(getAddr $DELEGATOR) + TX=$(echo qwertyuiop | ${CLIENT_EXE} tx send --sequence=2 --amount=15fermion --to=$DELA_ADDR --name=$RICH) + txSucceeded $? "$TX" "$DELA_ADDR" + TX_HEIGHT=$(echo $TX | jq .height) + set +u ; checkAccount $DELA_ADDR "15" $TX_HEIGHT ; set -u + + # delegate some coins to the new + TX=$(echo qwertyuiop | ${CLIENT_EXE} tx delegate --sequence=1 --amount=10fermion --name=$DELEGATOR --pubkey=$PK2) + if [ $? != 0 ]; then return 1; fi + TX_HEIGHT=$(echo $TX | jq .height) + set +u ; checkAccount $DELA_ADDR "5" $TX_HEIGHT ; set -u + checkCandidate $PK2 "20" + checkDelegatorBond $DELA_ADDR $PK2 "10" + + TX=$(echo qwertyuiop | ${CLIENT_EXE} tx delegate --sequence=2 --amount=3fermion --name=$DELEGATOR --pubkey=$PK2) + if [ $? != 0 ]; then return 1; fi + TX_HEIGHT=$(echo $TX | jq .height) + set +u ; checkAccount $DELA_ADDR "2" $TX_HEIGHT ; set -u + checkCandidate $PK2 "23" + checkDelegatorBond $DELA_ADDR $PK2 "13" + + # attempt a delegation without enough funds + # NOTE the sequence number still increments here because it will fail + # only during DeliverTx - however this should be updated (TODO) in new + # SDK when we can fail in CheckTx + TX=$(echo qwertyuiop | ${CLIENT_EXE} tx delegate --sequence=3 --amount=3fermion --name=$DELEGATOR --pubkey=$PK2 2>/dev/null) + if [ $? == 0 ]; then return 1; fi + TX_HEIGHT=$(echo $TX | jq .height) + set +u ; checkAccount $DELA_ADDR "2" $TX_HEIGHT ; set -u + checkCandidate $PK2 "23" + checkDelegatorBond $DELA_ADDR $PK2 "13" + + # perform the final delegation which should empty the delegators account + TX=$(echo qwertyuiop | ${CLIENT_EXE} tx delegate --sequence=4 --amount=2fermion --name=$DELEGATOR --pubkey=$PK2) + if [ $? != 0 ]; then return 1; fi + TX_HEIGHT=$(echo $TX | jq .height) + set +u ; checkAccount $DELA_ADDR "null" $TX_HEIGHT ; set -u #empty account is null + checkCandidate $PK2 "25" +} + +test04Unbond() { + # unbond from the delegator a bit + TX=$(echo qwertyuiop | ${CLIENT_EXE} tx unbond --sequence=5 --shares=10 --name=$DELEGATOR --pubkey=$PK2) + TX_HEIGHT=$(echo $TX | jq .height) + set +u ; checkAccount $DELA_ADDR "10" $TX_HEIGHT ; set -u + checkCandidate $PK2 "15" + checkDelegatorBond $DELA_ADDR $PK2 "5" + + # attempt to unbond more shares than exist + TX=$(echo qwertyuiop | ${CLIENT_EXE} tx unbond --sequence=6 --shares=10 --name=$DELEGATOR --pubkey=$PK2 2>/dev/null) + if [ $? == 0 ]; then return 1; fi + TX_HEIGHT=$(echo $TX | jq .height) + set +u ; checkAccount $DELA_ADDR "10" $TX_HEIGHT ; set -u + checkCandidate $PK2 "15" + checkDelegatorBond $DELA_ADDR $PK2 "5" + + # unbond entirely from the delegator + TX=$(echo qwertyuiop | ${CLIENT_EXE} tx unbond --sequence=6 --shares=5 --name=$DELEGATOR --pubkey=$PK2) + TX_HEIGHT=$(echo $TX | jq .height) + set +u ; checkAccount $DELA_ADDR "15" $TX_HEIGHT ; set -u + checkCandidate $PK2 "10" + checkDelegatorBondEmpty $DELA_ADDR $PK2 + + # unbond a bit from the owner + TX=$(echo qwertyuiop | ${CLIENT_EXE} tx unbond --sequence=2 --shares=5 --name=$POOR --pubkey=$PK2) + TX_HEIGHT=$(echo $TX | jq .height) + set +u ; checkAccount $CAND_ADDR "987" $TX_HEIGHT ; set -u + checkCandidate $PK2 "5" + checkDelegatorBond $CAND_ADDR $PK2 "5" + + # attempt to unbond more shares than exist + TX=$(echo qwertyuiop | ${CLIENT_EXE} tx unbond --sequence=3 --shares=10 --name=$POOR --pubkey=$PK2 2>/dev/null) + if [ $? == 0 ]; then return 1; fi + TX_HEIGHT=$(echo $TX | jq .height) + set +u ; checkAccount $CAND_ADDR "987" $TX_HEIGHT ; set -u + checkCandidate $PK2 "5" + checkDelegatorBond $CAND_ADDR $PK2 "5" + + # unbond entirely from the validator + TX=$(echo qwertyuiop | ${CLIENT_EXE} tx unbond --sequence=3 --shares=5 --name=$POOR --pubkey=$PK2) + TX_HEIGHT=$(echo $TX | jq .height) + set +u ; checkAccount $CAND_ADDR "992" $TX_HEIGHT ; set -u + checkCandidateEmpty $PK2 + checkDelegatorBondEmpty $CAND_ADDR $PK2 +} + +# Load common then run these tests with shunit2! +CLI_DIR=$GOPATH/src/github.com/cosmos/gaia/vendor/github.com/cosmos/cosmos-sdk/tests/cli + +. $CLI_DIR/common.sh +. $CLI_DIR/shunit2 diff --git a/x/stake/commands/query.go b/x/stake/commands/query.go new file mode 100644 index 0000000000..2528d07d10 --- /dev/null +++ b/x/stake/commands/query.go @@ -0,0 +1,136 @@ +package commands + +import ( + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + "github.com/spf13/viper" + + crypto "github.com/tendermint/go-crypto" + + "github.com/cosmos/cosmos-sdk/client/commands" + "github.com/cosmos/cosmos-sdk/client/commands/query" + "github.com/cosmos/cosmos-sdk/modules/coin" + "github.com/cosmos/cosmos-sdk/stack" + "github.com/cosmos/gaia/modules/stake" +) + +//nolint +var ( + CmdQueryCandidates = &cobra.Command{ + Use: "candidates", + Short: "Query for the set of validator-candidates pubkeys", + RunE: cmdQueryCandidates, + } + + CmdQueryCandidate = &cobra.Command{ + Use: "candidate", + Short: "Query a validator-candidate account", + RunE: cmdQueryCandidate, + } + + CmdQueryDelegatorBond = &cobra.Command{ + Use: "delegator-bond", + Short: "Query a delegators bond based on address and candidate pubkey", + RunE: cmdQueryDelegatorBond, + } + + CmdQueryDelegatorCandidates = &cobra.Command{ + Use: "delegator-candidates", + RunE: cmdQueryDelegatorCandidates, + Short: "Query all delegators candidates' pubkeys based on address", + } + + FlagDelegatorAddress = "delegator-address" +) + +func init() { + //Add Flags + fsPk := flag.NewFlagSet("", flag.ContinueOnError) + fsPk.String(FlagPubKey, "", "PubKey of the validator-candidate") + fsAddr := flag.NewFlagSet("", flag.ContinueOnError) + fsAddr.String(FlagDelegatorAddress, "", "Delegator Hex Address") + + CmdQueryCandidate.Flags().AddFlagSet(fsPk) + CmdQueryDelegatorBond.Flags().AddFlagSet(fsPk) + CmdQueryDelegatorBond.Flags().AddFlagSet(fsAddr) + CmdQueryDelegatorCandidates.Flags().AddFlagSet(fsAddr) +} + +func cmdQueryCandidates(cmd *cobra.Command, args []string) error { + + var pks []crypto.PubKey + + prove := !viper.GetBool(commands.FlagTrustNode) + key := stack.PrefixedKey(stake.Name(), stake.CandidatesPubKeysKey) + height, err := query.GetParsed(key, &pks, query.GetHeight(), prove) + if err != nil { + return err + } + + return query.OutputProof(pks, height) +} + +func cmdQueryCandidate(cmd *cobra.Command, args []string) error { + + var candidate stake.Candidate + + pk, err := GetPubKey(viper.GetString(FlagPubKey)) + if err != nil { + return err + } + + prove := !viper.GetBool(commands.FlagTrustNode) + key := stack.PrefixedKey(stake.Name(), stake.GetCandidateKey(pk)) + height, err := query.GetParsed(key, &candidate, query.GetHeight(), prove) + if err != nil { + return err + } + + return query.OutputProof(candidate, height) +} + +func cmdQueryDelegatorBond(cmd *cobra.Command, args []string) error { + + var bond stake.DelegatorBond + + pk, err := GetPubKey(viper.GetString(FlagPubKey)) + if err != nil { + return err + } + + delegatorAddr := viper.GetString(FlagDelegatorAddress) + delegator, err := commands.ParseActor(delegatorAddr) + if err != nil { + return err + } + delegator = coin.ChainAddr(delegator) + + prove := !viper.GetBool(commands.FlagTrustNode) + key := stack.PrefixedKey(stake.Name(), stake.GetDelegatorBondKey(delegator, pk)) + height, err := query.GetParsed(key, &bond, query.GetHeight(), prove) + if err != nil { + return err + } + + return query.OutputProof(bond, height) +} + +func cmdQueryDelegatorCandidates(cmd *cobra.Command, args []string) error { + + delegatorAddr := viper.GetString(FlagDelegatorAddress) + delegator, err := commands.ParseActor(delegatorAddr) + if err != nil { + return err + } + delegator = coin.ChainAddr(delegator) + + prove := !viper.GetBool(commands.FlagTrustNode) + key := stack.PrefixedKey(stake.Name(), stake.GetDelegatorBondsKey(delegator)) + var candidates []crypto.PubKey + height, err := query.GetParsed(key, &candidates, query.GetHeight(), prove) + if err != nil { + return err + } + + return query.OutputProof(candidates, height) +} diff --git a/x/stake/commands/tx.go b/x/stake/commands/tx.go new file mode 100644 index 0000000000..e14ac8cec1 --- /dev/null +++ b/x/stake/commands/tx.go @@ -0,0 +1,195 @@ +package commands + +import ( + "encoding/hex" + "fmt" + + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + "github.com/spf13/viper" + + crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/tmlibs/rational" + + txcmd "github.com/cosmos/cosmos-sdk/client/commands/txs" + "github.com/cosmos/cosmos-sdk/modules/coin" + + "github.com/cosmos/gaia/modules/stake" +) + +// nolint +const ( + FlagPubKey = "pubkey" + FlagAmount = "amount" + FlagShares = "shares" + + FlagMoniker = "moniker" + FlagIdentity = "keybase-sig" + FlagWebsite = "website" + FlagDetails = "details" +) + +// nolint +var ( + CmdDeclareCandidacy = &cobra.Command{ + Use: "declare-candidacy", + Short: "create new validator-candidate account and delegate some coins to it", + RunE: cmdDeclareCandidacy, + } + CmdEditCandidacy = &cobra.Command{ + Use: "edit-candidacy", + Short: "edit and existing validator-candidate account", + RunE: cmdEditCandidacy, + } + CmdDelegate = &cobra.Command{ + Use: "delegate", + Short: "delegate coins to an existing validator/candidate", + RunE: cmdDelegate, + } + CmdUnbond = &cobra.Command{ + Use: "unbond", + Short: "unbond coins from a validator/candidate", + RunE: cmdUnbond, + } +) + +func init() { + + // define the flags + fsPk := flag.NewFlagSet("", flag.ContinueOnError) + fsPk.String(FlagPubKey, "", "PubKey of the validator-candidate") + + fsAmount := flag.NewFlagSet("", flag.ContinueOnError) + fsAmount.String(FlagAmount, "1fermion", "Amount of coins to bond") + + fsShares := flag.NewFlagSet("", flag.ContinueOnError) + fsShares.String(FlagShares, "", "Amount of shares to unbond, either in decimal or keyword MAX (ex. 1.23456789, 99, MAX)") + + fsCandidate := flag.NewFlagSet("", flag.ContinueOnError) + fsCandidate.String(FlagMoniker, "", "validator-candidate name") + fsCandidate.String(FlagIdentity, "", "optional keybase signature") + fsCandidate.String(FlagWebsite, "", "optional website") + fsCandidate.String(FlagDetails, "", "optional detailed description space") + + // add the flags + CmdDelegate.Flags().AddFlagSet(fsPk) + CmdDelegate.Flags().AddFlagSet(fsAmount) + + CmdUnbond.Flags().AddFlagSet(fsPk) + CmdUnbond.Flags().AddFlagSet(fsShares) + + CmdDeclareCandidacy.Flags().AddFlagSet(fsPk) + CmdDeclareCandidacy.Flags().AddFlagSet(fsAmount) + CmdDeclareCandidacy.Flags().AddFlagSet(fsCandidate) + + CmdEditCandidacy.Flags().AddFlagSet(fsPk) + CmdEditCandidacy.Flags().AddFlagSet(fsCandidate) +} + +func cmdDeclareCandidacy(cmd *cobra.Command, args []string) error { + amount, err := coin.ParseCoin(viper.GetString(FlagAmount)) + if err != nil { + return err + } + + pk, err := GetPubKey(viper.GetString(FlagPubKey)) + if err != nil { + return err + } + + if viper.GetString(FlagMoniker) == "" { + return fmt.Errorf("please enter a moniker for the validator-candidate using --moniker") + } + + description := stake.Description{ + Moniker: viper.GetString(FlagMoniker), + Identity: viper.GetString(FlagIdentity), + Website: viper.GetString(FlagWebsite), + Details: viper.GetString(FlagDetails), + } + + tx := stake.NewTxDeclareCandidacy(amount, pk, description) + return txcmd.DoTx(tx) +} + +func cmdEditCandidacy(cmd *cobra.Command, args []string) error { + + pk, err := GetPubKey(viper.GetString(FlagPubKey)) + if err != nil { + return err + } + + description := stake.Description{ + Moniker: viper.GetString(FlagMoniker), + Identity: viper.GetString(FlagIdentity), + Website: viper.GetString(FlagWebsite), + Details: viper.GetString(FlagDetails), + } + + tx := stake.NewTxEditCandidacy(pk, description) + return txcmd.DoTx(tx) +} + +func cmdDelegate(cmd *cobra.Command, args []string) error { + amount, err := coin.ParseCoin(viper.GetString(FlagAmount)) + if err != nil { + return err + } + + pk, err := GetPubKey(viper.GetString(FlagPubKey)) + if err != nil { + return err + } + + tx := stake.NewTxDelegate(amount, pk) + return txcmd.DoTx(tx) +} + +func cmdUnbond(cmd *cobra.Command, args []string) error { + + // TODO once go-wire refactored the shares can be broadcast as a Rat instead of a string + + // check the shares before broadcasting + sharesStr := viper.GetString(FlagShares) + var shares rational.Rat + if sharesStr != "MAX" { + var err error + shares, err = rational.NewFromDecimal(sharesStr) + if err != nil { + return err + } + if !shares.GT(rational.Zero) { + return fmt.Errorf("shares must be positive integer or decimal (ex. 123, 1.23456789)") + } + } + + pk, err := GetPubKey(viper.GetString(FlagPubKey)) + if err != nil { + return err + } + + tx := stake.NewTxUnbond(sharesStr, pk) + return txcmd.DoTx(tx) +} + +// GetPubKey - create the pubkey from a pubkey string +func GetPubKey(pubKeyStr string) (pk crypto.PubKey, err error) { + + if len(pubKeyStr) == 0 { + err = fmt.Errorf("must use --pubkey flag") + return + } + if len(pubKeyStr) != 64 { //if len(pkBytes) != 32 { + err = fmt.Errorf("pubkey must be Ed25519 hex encoded string which is 64 characters long") + return + } + var pkBytes []byte + pkBytes, err = hex.DecodeString(pubKeyStr) + if err != nil { + return + } + var pkEd crypto.PubKeyEd25519 + copy(pkEd[:], pkBytes[:]) + pk = pkEd.Wrap() + return +} diff --git a/x/stake/errors.go b/x/stake/errors.go new file mode 100644 index 0000000000..900077b125 --- /dev/null +++ b/x/stake/errors.go @@ -0,0 +1,53 @@ +// nolint +package stake + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/errors" +) + +var ( + errCandidateEmpty = fmt.Errorf("Cannot bond to an empty candidate") + errBadBondingDenom = fmt.Errorf("Invalid coin denomination") + errBadBondingAmount = fmt.Errorf("Amount must be > 0") + errNoBondingAcct = fmt.Errorf("No bond account for this (address, validator) pair") + errCommissionNegative = fmt.Errorf("Commission must be positive") + errCommissionHuge = fmt.Errorf("Commission cannot be more than 100%") + + errBadValidatorAddr = fmt.Errorf("Validator does not exist for that address") + errCandidateExistsAddr = fmt.Errorf("Candidate already exist, cannot re-declare candidacy") + errMissingSignature = fmt.Errorf("Missing signature") + errBondNotNominated = fmt.Errorf("Cannot bond to non-nominated account") + errNoCandidateForAddress = fmt.Errorf("Validator does not exist for that address") + errNoDelegatorForAddress = fmt.Errorf("Delegator does not contain validator bond") + errInsufficientFunds = fmt.Errorf("Insufficient bond shares") + errBadRemoveValidator = fmt.Errorf("Error removing validator") + + invalidInput = errors.CodeTypeBaseInvalidInput +) + +func ErrBadValidatorAddr() error { + return errors.WithCode(errBadValidatorAddr, errors.CodeTypeBaseUnknownAddress) +} +func ErrCandidateExistsAddr() error { + return errors.WithCode(errCandidateExistsAddr, errors.CodeTypeBaseInvalidInput) +} +func ErrMissingSignature() error { + return errors.WithCode(errMissingSignature, errors.CodeTypeUnauthorized) +} +func ErrBondNotNominated() error { + return errors.WithCode(errBondNotNominated, errors.CodeTypeBaseInvalidOutput) +} +func ErrNoCandidateForAddress() error { + return errors.WithCode(errNoCandidateForAddress, errors.CodeTypeBaseUnknownAddress) +} +func ErrNoDelegatorForAddress() error { + return errors.WithCode(errNoDelegatorForAddress, errors.CodeTypeBaseInvalidInput) +} +func ErrInsufficientFunds() error { + return errors.WithCode(errInsufficientFunds, errors.CodeTypeBaseInvalidInput) +} +func ErrBadRemoveValidator() error { + return errors.WithCode(errBadRemoveValidator, errors.CodeTypeInternalErr) +} diff --git a/x/stake/handler.go b/x/stake/handler.go new file mode 100644 index 0000000000..c60b408017 --- /dev/null +++ b/x/stake/handler.go @@ -0,0 +1,529 @@ +package stake + +import ( + "fmt" + "strconv" + + "github.com/spf13/viper" + "github.com/tendermint/tmlibs/log" + "github.com/tendermint/tmlibs/rational" + + "github.com/cosmos/cosmos-sdk" + "github.com/cosmos/cosmos-sdk/errors" + "github.com/cosmos/cosmos-sdk/modules/auth" + "github.com/cosmos/cosmos-sdk/modules/coin" + "github.com/cosmos/cosmos-sdk/stack" + "github.com/cosmos/cosmos-sdk/state" +) + +// 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 { + stack.PassInitValidate +} + +var _ stack.Dispatchable = Handler{} // enforce interface at compile time + +// NewHandler returns a new Handler with the default Params +func NewHandler() Handler { + return Handler{} +} + +// Name - return stake namespace +func (Handler) Name() string { + return stakingModuleName +} + +// AssertDispatcher - placeholder for stack.Dispatchable +func (Handler) AssertDispatcher() {} + +// InitState - set genesis parameters for staking +func (h Handler) InitState(l log.Logger, store state.SimpleDB, + 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 state.SimpleDB) error { + if module != stakingModuleName { + return errors.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 errors.ErrUnknownKey(key) + } + + saveParams(store, params) + return nil +} + +// CheckTx checks if the tx is properly structured +func (h Handler) CheckTx(ctx sdk.Context, store state.SimpleDB, + 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, errors.ErrUnknownTxType(tx) +} + +// DeliverTx executes the tx if valid +func (h Handler) DeliverTx(ctx sdk.Context, store state.SimpleDB, + 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 state.SimpleDB + 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 state.SimpleDB + 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 state.SimpleDB) error { + if tx.Bond.Denom != loadParams(store).AllowedBondDenom { + return fmt.Errorf("Invalid coin denomination") + } + return nil +} + +//_____________________________________________________________________ + +type deliver struct { + store state.SimpleDB + 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 +} diff --git a/x/stake/handler_test.go b/x/stake/handler_test.go new file mode 100644 index 0000000000..690e7710cf --- /dev/null +++ b/x/stake/handler_test.go @@ -0,0 +1,336 @@ +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" + "github.com/cosmos/cosmos-sdk/modules/coin" + "github.com/cosmos/cosmos-sdk/state" +) + +//______________________________________________________________________ + +// 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(sender sdk.Actor, accStore map[string]int64) deliver { + store := state.NewMemKVStore() + 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) { + assert := assert.New(t) + senders, accStore := initAccounts(2, 1000) // for accounts + + deliverer := newDeliver(senders[0], accStore) + checker := check{ + store: deliverer.store, + sender: senders[0], + } + + txDeclareCandidacy := newTxDeclareCandidacy(10, pks[0]) + got := deliverer.declareCandidacy(txDeclareCandidacy) + assert.NoError(got, "expected no error on runTxDeclareCandidacy") + + // one sender can bond to two different pubKeys + txDeclareCandidacy.PubKey = pks[1] + err := checker.declareCandidacy(txDeclareCandidacy) + assert.Nil(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(err, "expected error on checkTx") +} + +func TestIncrementsTxDelegate(t *testing.T) { + assert := assert.New(t) + initSender := int64(1000) + senders, accStore := initAccounts(1, initSender) // for accounts + deliverer := newDeliver(senders[0], accStore) + + // first declare candidacy + bondAmount := int64(10) + txDeclareCandidacy := newTxDeclareCandidacy(bondAmount, pks[0]) + got := deliverer.declareCandidacy(txDeclareCandidacy) + assert.NoError(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(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(expectedBond, gotBonded, "i: %v, %v, %v", i, expectedBond, gotBonded) + assert.Equal(expectedBond, gotHolder, "i: %v, %v, %v", i, expectedBond, gotHolder) + assert.Equal(expectedSender, gotSender, "i: %v, %v, %v", i, expectedSender, gotSender) + } +} + +func TestIncrementsTxUnbond(t *testing.T) { + assert := assert.New(t) + initSender := int64(0) + senders, accStore := initAccounts(1, initSender) // for accounts + deliverer := newDeliver(senders[0], accStore) + + // set initial bond + initBond := int64(1000) + accStore[string(deliverer.sender.Address)] = initBond + got := deliverer.declareCandidacy(newTxDeclareCandidacy(initBond, pks[0])) + assert.NoError(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(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(expectedBond, gotBonded, "%v, %v", expectedBond, gotBonded) + assert.Equal(expectedBond, gotHolder, "%v, %v", expectedBond, gotHolder) + assert.Equal(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(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(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(got, "expected unbond tx to pass") +} + +func TestMultipleTxDeclareCandidacy(t *testing.T) { + assert := assert.New(t) + initSender := int64(1000) + senders, accStore := initAccounts(3, initSender) + pubKeys := []crypto.PubKey{pks[0], pks[1], pks[2]} + deliverer := newDeliver(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(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(i+1, len(candidates), "expected %d candidates got %d, candidates: %v", i+1, len(candidates), candidates) + assert.Equal(10, int(val.Liabilities.Evaluate()), "expected %d shares, got %d", 10, val.Liabilities) + assert.Equal(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(got, "expected tx %d to be ok, got %v", i, got) + + //Check that the account is unbonded + candidates := loadCandidates(deliverer.store) + assert.Equal(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(candidatePost, "expected nil candidate retrieve, got %d", 0, candidatePost) + assert.Equal(balanceExpd, balanceGot, "expected account to have %d, got %d", balanceExpd, balanceGot) + } +} + +func TestMultipleTxDelegate(t *testing.T) { + assert, require := assert.New(t), require.New(t) + accounts, accStore := initAccounts(3, 1000) + sender, delegators := accounts[0], accounts[1:] + deliverer := newDeliver(sender, accStore) + + //first make a candidate + txDeclareCandidacy := newTxDeclareCandidacy(10, pks[0]) + got := deliverer.declareCandidacy(txDeclareCandidacy) + require.NoError(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(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(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(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(bond, "expected delegatee bond %d to be nil", bond) + } +} + +func TestVoidCandidacy(t *testing.T) { + assert, require := assert.New(t), require.New(t) + accounts, accStore := initAccounts(2, 1000) // for accounts + sender, delegator := accounts[0], accounts[1] + deliverer := newDeliver(sender, accStore) + + // create the candidate + txDeclareCandidacy := newTxDeclareCandidacy(10, pks[0]) + got := deliverer.declareCandidacy(txDeclareCandidacy) + require.NoError(got, "expected no error on runTxDeclareCandidacy") + + // bond a delegator + txDelegate := newTxDelegate(10, pks[0]) + deliverer.sender = delegator + got = deliverer.delegate(txDelegate) + require.NoError(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(got, "expected no error on runTxDeclareCandidacy") + + // test that this pubkey cannot yet be bonded too + deliverer.sender = delegator + got = deliverer.delegate(txDelegate) + assert.Error(got, "expected error, got %v", got) + + // test that the delegator can still withdraw their bonds + got = deliverer.unbond(txUndelegate) + require.NoError(got, "expected no error on runTxDeclareCandidacy") + + // verify that the pubkey can now be reused + got = deliverer.declareCandidacy(txDeclareCandidacy) + assert.NoError(got, "expected ok, got %v", got) + +} diff --git a/x/stake/rest/query.go b/x/stake/rest/query.go new file mode 100644 index 0000000000..48e54cfe81 --- /dev/null +++ b/x/stake/rest/query.go @@ -0,0 +1,188 @@ +package rest + +import ( + "fmt" + "net/http" + + "github.com/gorilla/mux" + "github.com/spf13/viper" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/commands" + "github.com/cosmos/cosmos-sdk/client/commands/query" + "github.com/cosmos/cosmos-sdk/modules/coin" + "github.com/cosmos/cosmos-sdk/stack" + + "github.com/cosmos/gaia/modules/stake" + scmds "github.com/cosmos/gaia/modules/stake/commands" + + crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/tmlibs/common" +) + +// RegisterQueryCandidate is a mux.Router handler that exposes GET +// method access on route /query/stake/candidate/{pubkey} to query a candidate +func RegisterQueryCandidate(r *mux.Router) error { + r.HandleFunc("/query/stake/candidate/{pubkey}", queryCandidate).Methods("GET") + return nil +} + +// RegisterQueryCandidates is a mux.Router handler that exposes GET +// method access on route /query/stake/candidate to query the group of all candidates +func RegisterQueryCandidates(r *mux.Router) error { + r.HandleFunc("/query/stake/candidates", queryCandidates).Methods("GET") + return nil +} + +// RegisterQueryDelegatorBond is a mux.Router handler that exposes GET +// method access on route /query/stake/candidate/{pubkey} to query a candidate +func RegisterQueryDelegatorBond(r *mux.Router) error { + r.HandleFunc("/query/stake/delegator/{address}/{pubkey}", queryDelegatorBond).Methods("GET") + return nil +} + +// RegisterQueryDelegatorCandidates is a mux.Router handler that exposes GET +// method access on route /query/stake/candidate to query the group of all candidates +func RegisterQueryDelegatorCandidates(r *mux.Router) error { + r.HandleFunc("/query/stake/delegator_candidates/{address}", queryDelegatorCandidates).Methods("GET") + return nil +} + +//--------------------------------------------------------------------- + +// queryCandidate is the HTTP handlerfunc to query a candidate +// it expects a query string +func queryCandidate(w http.ResponseWriter, r *http.Request) { + + // get the arguments object + args := mux.Vars(r) + prove := !viper.GetBool(commands.FlagTrustNode) // from viper because defined when starting server + + // get the pubkey + pkArg := args["pubkey"] + pk, err := scmds.GetPubKey(pkArg) + if err != nil { + common.WriteError(w, err) + return + } + + // get the candidate + var candidate stake.Candidate + key := stack.PrefixedKey(stake.Name(), stake.GetCandidateKey(pk)) + height, err := query.GetParsed(key, &candidate, query.GetHeight(), prove) + if client.IsNoDataErr(err) { + err := fmt.Errorf("candidate bytes are empty for pubkey: %q", pkArg) + common.WriteError(w, err) + return + } else if err != nil { + common.WriteError(w, err) + return + } + + // write the output + err = query.FoutputProof(w, candidate, height) + if err != nil { + common.WriteError(w, err) + } +} + +// queryCandidates is the HTTP handlerfunc to query the group of all candidates +func queryCandidates(w http.ResponseWriter, r *http.Request) { + + var pks []crypto.PubKey + + prove := !viper.GetBool(commands.FlagTrustNode) // from viper because defined when starting server + key := stack.PrefixedKey(stake.Name(), stake.CandidatesPubKeysKey) + height, err := query.GetParsed(key, &pks, query.GetHeight(), prove) + if err != nil { + common.WriteError(w, err) + return + } + + err = query.FoutputProof(w, pks, height) + if err != nil { + common.WriteError(w, err) + } +} + +// queryDelegatorBond is the HTTP handlerfunc to query a delegator bond it +// expects a query string +func queryDelegatorBond(w http.ResponseWriter, r *http.Request) { + + // get the arguments object + args := mux.Vars(r) + prove := !viper.GetBool(commands.FlagTrustNode) // from viper because defined when starting server + + // get the pubkey + pkArg := args["pubkey"] + pk, err := scmds.GetPubKey(pkArg) + if err != nil { + common.WriteError(w, err) + return + } + + // get the delegator actor + delegatorAddr := args["address"] + delegator, err := commands.ParseActor(delegatorAddr) + if err != nil { + common.WriteError(w, err) + return + } + delegator = coin.ChainAddr(delegator) + + // get the bond + var bond stake.DelegatorBond + key := stack.PrefixedKey(stake.Name(), stake.GetDelegatorBondKey(delegator, pk)) + height, err := query.GetParsed(key, &bond, query.GetHeight(), prove) + if client.IsNoDataErr(err) { + err := fmt.Errorf("bond bytes are empty for pubkey: %q, address: %q", pkArg, delegatorAddr) + common.WriteError(w, err) + return + } else if err != nil { + common.WriteError(w, err) + return + } + + // write the output + err = query.FoutputProof(w, bond, height) + if err != nil { + common.WriteError(w, err) + } +} + +// queryDelegatorCandidates is the HTTP handlerfunc to query a delegator bond it +// expects a query string +func queryDelegatorCandidates(w http.ResponseWriter, r *http.Request) { + + // get the arguments object + args := mux.Vars(r) + prove := !viper.GetBool(commands.FlagTrustNode) // from viper because defined when starting server + + // get the delegator actor + delegatorAddr := args["address"] + delegator, err := commands.ParseActor(delegatorAddr) + if err != nil { + common.WriteError(w, err) + return + } + delegator = coin.ChainAddr(delegator) + + // get the bond + var bond stake.DelegatorBond + key := stack.PrefixedKey(stake.Name(), stake.GetDelegatorBondsKey(delegator)) + height, err := query.GetParsed(key, &bond, query.GetHeight(), prove) + if client.IsNoDataErr(err) { + err := fmt.Errorf("bond bytes are empty for address: %q", delegatorAddr) + common.WriteError(w, err) + return + } else if err != nil { + common.WriteError(w, err) + return + } + + // write the output + err = query.FoutputProof(w, bond, height) + if err != nil { + common.WriteError(w, err) + } +} diff --git a/x/stake/rest/tx.go b/x/stake/rest/tx.go new file mode 100644 index 0000000000..090ea5abbc --- /dev/null +++ b/x/stake/rest/tx.go @@ -0,0 +1,159 @@ +package rest + +import ( + "net/http" + "strings" + + "github.com/gorilla/mux" + crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/tmlibs/common" + + sdk "github.com/cosmos/cosmos-sdk" + "github.com/cosmos/cosmos-sdk/client/commands" + "github.com/cosmos/cosmos-sdk/modules/auth" + "github.com/cosmos/cosmos-sdk/modules/base" + "github.com/cosmos/cosmos-sdk/modules/coin" + "github.com/cosmos/cosmos-sdk/modules/fee" + "github.com/cosmos/cosmos-sdk/modules/nonce" + "github.com/cosmos/gaia/modules/stake" +) + +const ( + //parameters used in urls + paramPubKey = "pubkey" + paramAmount = "amount" + paramShares = "shares" + + paramName = "name" + paramKeybase = "keybase" + paramWebsite = "website" + paramDetails = "details" +) + +type delegateInput struct { + Fees *coin.Coin `json:"fees"` + Sequence uint32 `json:"sequence"` + + Pubkey crypto.PubKey `json:"pub_key"` + From *sdk.Actor `json:"from"` + Amount coin.Coin `json:"amount"` +} + +type unbondInput struct { + Fees *coin.Coin `json:"fees"` + Sequence uint32 `json:"sequence"` + + Pubkey crypto.PubKey `json:"pub_key"` + From *sdk.Actor `json:"from"` + Shares string `json:"amount"` +} + +// RegisterDelegate is a mux.Router handler that exposes +// POST method access on route /tx/stake/delegate to create a +// transaction for delegate to a candidaate/validator +func RegisterDelegate(r *mux.Router) error { + r.HandleFunc("/build/stake/delegate", delegate).Methods("POST") + return nil +} + +// RegisterUnbond is a mux.Router handler that exposes +// POST method access on route /tx/stake/unbond to create a +// transaction for unbonding delegated coins +func RegisterUnbond(r *mux.Router) error { + r.HandleFunc("/build/stake/unbond", unbond).Methods("POST") + return nil +} + +func prepareDelegateTx(di *delegateInput) sdk.Tx { + tx := stake.NewTxDelegate(di.Amount, di.Pubkey) + // fees are optional + if di.Fees != nil && !di.Fees.IsZero() { + tx = fee.NewFee(tx, *di.Fees, *di.From) + } + // only add the actual signer to the nonce + signers := []sdk.Actor{*di.From} + tx = nonce.NewTx(di.Sequence, signers, tx) + tx = base.NewChainTx(commands.GetChainID(), 0, tx) + + tx = auth.NewSig(tx).Wrap() + return tx +} + +func delegate(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + di := new(delegateInput) + if err := common.ParseRequestAndValidateJSON(r, di); err != nil { + common.WriteError(w, err) + return + } + + var errsList []string + if di.From == nil { + errsList = append(errsList, `"from" cannot be nil`) + } + if di.Sequence <= 0 { + errsList = append(errsList, `"sequence" must be > 0`) + } + if di.Pubkey.Empty() { + errsList = append(errsList, `"pubkey" cannot be empty`) + } + if len(errsList) > 0 { + code := http.StatusBadRequest + err := &common.ErrorResponse{ + Err: strings.Join(errsList, ", "), + Code: code, + } + common.WriteCode(w, err, code) + return + } + + tx := prepareDelegateTx(di) + common.WriteSuccess(w, tx) +} + +func prepareUnbondTx(ui *unbondInput) sdk.Tx { + tx := stake.NewTxUnbond(ui.Shares, ui.Pubkey) + // fees are optional + if ui.Fees != nil && !ui.Fees.IsZero() { + tx = fee.NewFee(tx, *ui.Fees, *ui.From) + } + // only add the actual signer to the nonce + signers := []sdk.Actor{*ui.From} + tx = nonce.NewTx(ui.Sequence, signers, tx) + tx = base.NewChainTx(commands.GetChainID(), 0, tx) + + tx = auth.NewSig(tx).Wrap() + return tx +} + +func unbond(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + ui := new(unbondInput) + if err := common.ParseRequestAndValidateJSON(r, ui); err != nil { + common.WriteError(w, err) + return + } + + var errsList []string + if ui.From == nil { + errsList = append(errsList, `"from" cannot be nil`) + } + if ui.Sequence <= 0 { + errsList = append(errsList, `"sequence" must be > 0`) + } + if ui.Pubkey.Empty() { + errsList = append(errsList, `"pubkey" cannot be empty`) + } + if len(errsList) > 0 { + code := http.StatusBadRequest + err := &common.ErrorResponse{ + Err: strings.Join(errsList, ", "), + Code: code, + } + common.WriteCode(w, err, code) + return + } + + tx := prepareUnbondTx(ui) + common.WriteSuccess(w, tx) +} diff --git a/x/stake/state.go b/x/stake/state.go new file mode 100644 index 0000000000..e037d837b2 --- /dev/null +++ b/x/stake/state.go @@ -0,0 +1,273 @@ +package stake + +import ( + "encoding/json" + + crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/go-wire" + + sdk "github.com/cosmos/cosmos-sdk" + "github.com/cosmos/cosmos-sdk/state" +) + +// nolint +var ( + // Keys for store prefixes + CandidatesPubKeysKey = []byte{0x01} // key for all candidates' pubkeys + ParamKey = []byte{0x02} // key for global parameters relating to staking + GlobalStateKey = []byte{0x03} // key for global parameters relating to staking + + // Key prefixes + CandidateKeyPrefix = []byte{0x04} // prefix for each key to a candidate + DelegatorBondKeyPrefix = []byte{0x05} // prefix for each key to a delegator's bond + DelegatorBondsKeyPrefix = []byte{0x06} // prefix for each key to a delegator's bond +) + +// GetCandidateKey - get the key for the candidate with pubKey +func GetCandidateKey(pubKey crypto.PubKey) []byte { + return append(CandidateKeyPrefix, pubKey.Bytes()...) +} + +// GetDelegatorBondKey - get the key for delegator bond with candidate +func GetDelegatorBondKey(delegator sdk.Actor, candidate crypto.PubKey) []byte { + return append(GetDelegatorBondKeyPrefix(delegator), candidate.Bytes()...) +} + +// GetDelegatorBondKeyPrefix - get the prefix for a delegator for all candidates +func GetDelegatorBondKeyPrefix(delegator sdk.Actor) []byte { + return append(DelegatorBondKeyPrefix, wire.BinaryBytes(&delegator)...) +} + +// GetDelegatorBondsKey - get the key for list of all the delegator's bonds +func GetDelegatorBondsKey(delegator sdk.Actor) []byte { + return append(DelegatorBondsKeyPrefix, wire.BinaryBytes(&delegator)...) +} + +//--------------------------------------------------------------------- + +// Get the active list of all the candidate pubKeys and owners +func loadCandidatesPubKeys(store state.SimpleDB) (pubKeys []crypto.PubKey) { + bytes := store.Get(CandidatesPubKeysKey) + if bytes == nil { + return + } + err := wire.ReadBinaryBytes(bytes, &pubKeys) + if err != nil { + panic(err) + } + return +} +func saveCandidatesPubKeys(store state.SimpleDB, pubKeys []crypto.PubKey) { + b := wire.BinaryBytes(pubKeys) + store.Set(CandidatesPubKeysKey, b) +} + +// loadCandidates - get the active list of all candidates TODO replace with multistore +func loadCandidates(store state.SimpleDB) (candidates Candidates) { + pks := loadCandidatesPubKeys(store) + for _, pk := range pks { + candidates = append(candidates, loadCandidate(store, pk)) + } + return +} + +//--------------------------------------------------------------------- + +// loadCandidate - loads the candidate object for the provided pubkey +func loadCandidate(store state.SimpleDB, pubKey crypto.PubKey) *Candidate { + if pubKey.Empty() { + return nil + } + b := store.Get(GetCandidateKey(pubKey)) + if b == nil { + return nil + } + candidate := new(Candidate) + err := json.Unmarshal(b, candidate) + if err != nil { + panic(err) // This error should never occure big problem if does + } + return candidate +} + +func saveCandidate(store state.SimpleDB, candidate *Candidate) { + + if !store.Has(GetCandidateKey(candidate.PubKey)) { + // TODO to be replaced with iteration in the multistore? + pks := loadCandidatesPubKeys(store) + saveCandidatesPubKeys(store, append(pks, candidate.PubKey)) + } + + b, err := json.Marshal(*candidate) + if err != nil { + panic(err) + } + store.Set(GetCandidateKey(candidate.PubKey), b) +} + +func removeCandidate(store state.SimpleDB, pubKey crypto.PubKey) { + store.Remove(GetCandidateKey(pubKey)) + + // TODO to be replaced with iteration in the multistore? + pks := loadCandidatesPubKeys(store) + for i := range pks { + if pks[i].Equals(pubKey) { + saveCandidatesPubKeys(store, + append(pks[:i], pks[i+1:]...)) + break + } + } +} + +//--------------------------------------------------------------------- + +// load the pubkeys of all candidates a delegator is delegated too +func loadDelegatorCandidates(store state.SimpleDB, + delegator sdk.Actor) (candidates []crypto.PubKey) { + + candidateBytes := store.Get(GetDelegatorBondsKey(delegator)) + if candidateBytes == nil { + return nil + } + + err := wire.ReadBinaryBytes(candidateBytes, &candidates) + if err != nil { + panic(err) + } + return +} + +//--------------------------------------------------------------------- + +func loadDelegatorBond(store state.SimpleDB, + delegator sdk.Actor, candidate crypto.PubKey) *DelegatorBond { + + delegatorBytes := store.Get(GetDelegatorBondKey(delegator, candidate)) + if delegatorBytes == nil { + return nil + } + + bond := new(DelegatorBond) + err := json.Unmarshal(delegatorBytes, bond) + if err != nil { + panic(err) + } + return bond +} + +func saveDelegatorBond(store state.SimpleDB, delegator sdk.Actor, bond *DelegatorBond) { + + // if a new bond add to the list of bonds + if loadDelegatorBond(store, delegator, bond.PubKey) == nil { + pks := loadDelegatorCandidates(store, delegator) + pks = append(pks, (*bond).PubKey) + b := wire.BinaryBytes(pks) + store.Set(GetDelegatorBondsKey(delegator), b) + } + + // now actually save the bond + b, err := json.Marshal(*bond) + if err != nil { + panic(err) + } + store.Set(GetDelegatorBondKey(delegator, bond.PubKey), b) + //updateDelegatorBonds(store, delegator) +} + +func removeDelegatorBond(store state.SimpleDB, delegator sdk.Actor, candidate crypto.PubKey) { + + // TODO use list queries on multistore to remove iterations here! + // first remove from the list of bonds + pks := loadDelegatorCandidates(store, delegator) + for i, pk := range pks { + if candidate.Equals(pk) { + pks = append(pks[:i], pks[i+1:]...) + } + } + b := wire.BinaryBytes(pks) + store.Set(GetDelegatorBondsKey(delegator), b) + + // now remove the actual bond + store.Remove(GetDelegatorBondKey(delegator, candidate)) + //updateDelegatorBonds(store, delegator) +} + +//func updateDelegatorBonds(store state.SimpleDB, +//delegator sdk.Actor) { + +//var bonds []*DelegatorBond + +//prefix := GetDelegatorBondKeyPrefix(delegator) +//l := len(prefix) +//delegatorsBytes := store.List(prefix, +//append(prefix[:l-1], (prefix[l-1]+1)), loadParams(store).MaxVals) + +//for _, delegatorBytesModel := range delegatorsBytes { +//delegatorBytes := delegatorBytesModel.Value +//if delegatorBytes == nil { +//continue +//} + +//bond := new(DelegatorBond) +//err := wire.ReadBinaryBytes(delegatorBytes, bond) +//if err != nil { +//panic(err) +//} +//bonds = append(bonds, bond) +//} + +//if len(bonds) == 0 { +//store.Remove(GetDelegatorBondsKey(delegator)) +//return +//} + +//b := wire.BinaryBytes(bonds) +//store.Set(GetDelegatorBondsKey(delegator), b) +//} + +//_______________________________________________________________________ + +// load/save the global staking params +func loadParams(store state.SimpleDB) (params Params) { + b := store.Get(ParamKey) + if b == nil { + return defaultParams() + } + + err := json.Unmarshal(b, ¶ms) + if err != nil { + panic(err) // This error should never occure big problem if does + } + + return +} +func saveParams(store state.SimpleDB, params Params) { + b, err := json.Marshal(params) + if err != nil { + panic(err) + } + store.Set(ParamKey, b) +} + +//_______________________________________________________________________ + +// load/save the global staking state +func loadGlobalState(store state.SimpleDB) (gs *GlobalState) { + b := store.Get(GlobalStateKey) + if b == nil { + return initialGlobalState() + } + gs = new(GlobalState) + err := json.Unmarshal(b, gs) + if err != nil { + panic(err) // This error should never occure big problem if does + } + return +} +func saveGlobalState(store state.SimpleDB, gs *GlobalState) { + b, err := json.Marshal(*gs) + if err != nil { + panic(err) + } + store.Set(GlobalStateKey, b) +} diff --git a/x/stake/state_test.go b/x/stake/state_test.go new file mode 100644 index 0000000000..919dfc4c44 --- /dev/null +++ b/x/stake/state_test.go @@ -0,0 +1,116 @@ +package stake + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk" + "github.com/cosmos/cosmos-sdk/modules/auth" + "github.com/cosmos/cosmos-sdk/store" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tendermint/tmlibs/rational" +) + +func TestState(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + db, err := dbm.NewGoLevelDB("basecoin", "basecoin-data") + require.Nil(err) + mainLoader := store.NewIAVLStoreLoader(int64(100), 10000, numHistory) + var mainStoreKey = sdk.NewKVStoreKey("main") + multiStore := store.NewCommitMultiStore(db) + multiStore.SetSubstoreLoader(mainStoreKey, mainLoader) + var store = auth.NewAccountStore(mainStoreKey, bcm.AppAccountCodec{}) + + delegator := sdk.Actor{"testChain", "testapp", []byte("addressdelegator")} + validator := sdk.Actor{"testChain", "testapp", []byte("addressvalidator")} + + pk := newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB57") + + //---------------------------------------------------------------------- + // Candidate checks + + // XXX expand to include both liabilities and assets use/test all candidate fields + candidate := &Candidate{ + Owner: validator, + PubKey: pk, + Assets: rational.New(9), + Liabilities: rational.New(9), + VotingPower: rational.Zero, + } + + candidatesEqual := func(c1, c2 *Candidate) bool { + return c1.Status == c2.Status && + c1.PubKey.Equals(c2.PubKey) && + c1.Owner.Equals(c2.Owner) && + c1.Assets.Equal(c2.Assets) && + c1.Liabilities.Equal(c2.Liabilities) && + c1.VotingPower.Equal(c2.VotingPower) && + c1.Description == c2.Description + } + + // check the empty store first + resCand := loadCandidate(store, pk) + assert.Nil(resCand) + resPks := loadCandidatesPubKeys(store) + assert.Zero(len(resPks)) + + // set and retrieve a record + saveCandidate(store, candidate) + resCand = loadCandidate(store, pk) + assert.True(candidatesEqual(candidate, resCand)) + + // modify a records, save, and retrieve + candidate.Liabilities = rational.New(99) + saveCandidate(store, candidate) + resCand = loadCandidate(store, pk) + assert.True(candidatesEqual(candidate, resCand)) + + // also test that the pubkey has been added to pubkey list + resPks = loadCandidatesPubKeys(store) + require.Equal(1, len(resPks)) + assert.Equal(pk, resPks[0]) + + //---------------------------------------------------------------------- + // Bond checks + + bond := &DelegatorBond{ + PubKey: pk, + Shares: rational.New(9), + } + + bondsEqual := func(b1, b2 *DelegatorBond) bool { + return b1.PubKey.Equals(b2.PubKey) && + b1.Shares.Equal(b2.Shares) + } + + //check the empty store first + resBond := loadDelegatorBond(store, delegator, pk) + assert.Nil(resBond) + + //Set and retrieve a record + saveDelegatorBond(store, delegator, bond) + resBond = loadDelegatorBond(store, delegator, pk) + assert.True(bondsEqual(bond, resBond)) + + //modify a records, save, and retrieve + bond.Shares = rational.New(99) + saveDelegatorBond(store, delegator, bond) + resBond = loadDelegatorBond(store, delegator, pk) + assert.True(bondsEqual(bond, resBond)) + + //---------------------------------------------------------------------- + // Param checks + + params := defaultParams() + + //check that the empty store loads the default + resParams := loadParams(store) + assert.Equal(params, resParams) + + //modify a params, save, and retrieve + params.MaxVals = 777 + saveParams(store, params) + resParams = loadParams(store) + assert.Equal(params, resParams) +} diff --git a/x/stake/test_common.go b/x/stake/test_common.go new file mode 100644 index 0000000000..17d26d9230 --- /dev/null +++ b/x/stake/test_common.go @@ -0,0 +1,91 @@ +package stake + +import ( + "encoding/hex" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + abci "github.com/tendermint/abci/types" + crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/tmlibs/rational" + + "github.com/cosmos/cosmos-sdk" +) + +func newActors(n int) (actors []sdk.Actor) { + for i := 0; i < n; i++ { + actors = append(actors, sdk.Actor{ + "testChain", "testapp", []byte(fmt.Sprintf("addr%d", i))}) + } + + return +} + +func newPubKey(pk string) crypto.PubKey { + pkBytes, _ := hex.DecodeString(pk) + var pkEd crypto.PubKeyEd25519 + copy(pkEd[:], pkBytes[:]) + return pkEd.Wrap() +} + +// dummy pubkeys used for testing +var pks = []crypto.PubKey{ + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB51"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB52"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB53"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB54"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB55"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB56"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB57"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB58"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB59"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB60"), +} + +// NOTE: PubKey is supposed to be the binaryBytes of the crypto.PubKey +// instead this is just being set the address here for testing purposes +func candidatesFromActors(actors []sdk.Actor, amts []int64) (candidates Candidates) { + for i := 0; i < len(actors); i++ { + c := &Candidate{ + Status: Unbonded, + PubKey: pks[i], + Owner: actors[i], + Assets: rational.New(amts[i]), + Liabilities: rational.New(amts[i]), + VotingPower: rational.New(amts[i]), + } + candidates = append(candidates, c) + } + + return +} + +func candidatesFromActorsEmpty(actors []sdk.Actor) (candidates Candidates) { + for i := 0; i < len(actors); i++ { + c := &Candidate{ + Status: Unbonded, + PubKey: pks[i], + Owner: actors[i], + Assets: rational.Zero, + Liabilities: rational.Zero, + VotingPower: rational.Zero, + } + candidates = append(candidates, c) + } + return +} + +// helper function test if Candidate is changed asabci.Validator +func testChange(t *testing.T, val Validator, chg *abci.Validator) { + assert := assert.New(t) + assert.Equal(val.PubKey.Bytes(), chg.PubKey) + assert.Equal(val.VotingPower.Evaluate(), chg.Power) +} + +// helper function test if Candidate is removed as abci.Validator +func testRemove(t *testing.T, val Validator, chg *abci.Validator) { + assert := assert.New(t) + assert.Equal(val.PubKey.Bytes(), chg.PubKey) + assert.Equal(int64(0), chg.Power) +} diff --git a/x/stake/tick.go b/x/stake/tick.go new file mode 100644 index 0000000000..678a01ccd4 --- /dev/null +++ b/x/stake/tick.go @@ -0,0 +1,75 @@ +package stake + +import ( + sdk "github.com/cosmos/cosmos-sdk" + "github.com/cosmos/cosmos-sdk/state" + + 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 state.SimpleDB) (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 state.SimpleDB, 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 +} diff --git a/x/stake/tick_test.go b/x/stake/tick_test.go new file mode 100644 index 0000000000..3a80ae2a3b --- /dev/null +++ b/x/stake/tick_test.go @@ -0,0 +1,120 @@ +package stake + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/tendermint/tmlibs/rational" + + "github.com/cosmos/cosmos-sdk/state" +) + +func TestGetInflation(t *testing.T) { + assert := assert.New(t) + store := state.NewMemKVStore() + 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(diffInflation.Equal(tc.expectedChange), + "%v, %v", diffInflation, tc.expectedChange) + } +} + +func TestProcessProvisions(t *testing.T) { + assert := assert.New(t) + store := state.NewMemKVStore() + 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(gs.bondedRatio().Equal(rational.New(bondedShares, totalSupply)), "%v", gs.bondedRatio()) + + // Supplies + assert.Equal(totalSupply, gs.TotalSupply) + assert.Equal(bondedShares, gs.BondedPool) + assert.Equal(unbondedShares, gs.UnbondedPool) + + // test the value of candidate shares + assert.True(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(startBondedPool+expProvisions, gs.BondedPool) + assert.Equal(startTotalSupply+expProvisions, gs.TotalSupply) + } + assert.NotEqual(initialSupply, gs.TotalSupply) + assert.Equal(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(gs.bondedRatio().Equal(rational.New(105906511, 305906511)), "%v", gs.bondedRatio()) + + // global supply + assert.Equal(int64(611813022), gs.TotalSupply) + assert.Equal(int64(211813022), gs.BondedPool) + assert.Equal(unbondedShares, gs.UnbondedPool) + + // test the value of candidate shares + assert.True(gs.bondedShareExRate().Mul(rational.New(bondedShares)).Equal(rational.New(211813022)), "%v", gs.bondedShareExRate()) + +} diff --git a/x/stake/tx.go b/x/stake/tx.go new file mode 100644 index 0000000000..951151dafd --- /dev/null +++ b/x/stake/tx.go @@ -0,0 +1,147 @@ +package stake + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk" + "github.com/cosmos/cosmos-sdk/modules/coin" + crypto "github.com/tendermint/go-crypto" +) + +// Tx +//-------------------------------------------------------------------------------- + +// register the tx type with its validation logic +// make sure to use the name of the handler as the prefix in the tx type, +// so it gets routed properly +const ( + ByteTxDeclareCandidacy = 0x55 + ByteTxEditCandidacy = 0x56 + ByteTxDelegate = 0x57 + ByteTxUnbond = 0x58 + TypeTxDeclareCandidacy = stakingModuleName + "/declareCandidacy" + TypeTxEditCandidacy = stakingModuleName + "/editCandidacy" + TypeTxDelegate = stakingModuleName + "/delegate" + TypeTxUnbond = stakingModuleName + "/unbond" +) + +func init() { + sdk.TxMapper.RegisterImplementation(TxDeclareCandidacy{}, TypeTxDeclareCandidacy, ByteTxDeclareCandidacy) + sdk.TxMapper.RegisterImplementation(TxEditCandidacy{}, TypeTxEditCandidacy, ByteTxEditCandidacy) + sdk.TxMapper.RegisterImplementation(TxDelegate{}, TypeTxDelegate, ByteTxDelegate) + sdk.TxMapper.RegisterImplementation(TxUnbond{}, TypeTxUnbond, ByteTxUnbond) +} + +//Verify interface at compile time +var _, _, _, _ sdk.TxInner = &TxDeclareCandidacy{}, &TxEditCandidacy{}, &TxDelegate{}, &TxUnbond{} + +// BondUpdate - struct for bonding or unbonding transactions +type BondUpdate struct { + PubKey crypto.PubKey `json:"pub_key"` + Bond coin.Coin `json:"amount"` +} + +// ValidateBasic - Check for non-empty candidate, and valid coins +func (tx BondUpdate) ValidateBasic() error { + if tx.PubKey.Empty() { + return errCandidateEmpty + } + + coins := coin.Coins{tx.Bond} + if !coins.IsValid() { + return coin.ErrInvalidCoins() + } + if !coins.IsPositive() { + return fmt.Errorf("Amount must be > 0") + } + return nil +} + +// TxDeclareCandidacy - struct for unbonding transactions +type TxDeclareCandidacy struct { + BondUpdate + Description +} + +// NewTxDeclareCandidacy - new TxDeclareCandidacy +func NewTxDeclareCandidacy(bond coin.Coin, pubKey crypto.PubKey, description Description) sdk.Tx { + return TxDeclareCandidacy{ + BondUpdate{ + PubKey: pubKey, + Bond: bond, + }, + description, + }.Wrap() +} + +// Wrap - Wrap a Tx as a Basecoin Tx +func (tx TxDeclareCandidacy) Wrap() sdk.Tx { return sdk.Tx{tx} } + +// TxEditCandidacy - struct for editing a candidate +type TxEditCandidacy struct { + PubKey crypto.PubKey `json:"pub_key"` + Description +} + +// NewTxEditCandidacy - new TxEditCandidacy +func NewTxEditCandidacy(pubKey crypto.PubKey, description Description) sdk.Tx { + return TxEditCandidacy{ + PubKey: pubKey, + Description: description, + }.Wrap() +} + +// Wrap - Wrap a Tx as a Basecoin Tx +func (tx TxEditCandidacy) Wrap() sdk.Tx { return sdk.Tx{tx} } + +// ValidateBasic - Check for non-empty candidate, +func (tx TxEditCandidacy) ValidateBasic() error { + if tx.PubKey.Empty() { + return errCandidateEmpty + } + + empty := Description{} + if tx.Description == empty { + return fmt.Errorf("Transaction must include some information to modify") + } + return nil +} + +// TxDelegate - struct for bonding transactions +type TxDelegate struct{ BondUpdate } + +// NewTxDelegate - new TxDelegate +func NewTxDelegate(bond coin.Coin, pubKey crypto.PubKey) sdk.Tx { + return TxDelegate{BondUpdate{ + PubKey: pubKey, + Bond: bond, + }}.Wrap() +} + +// Wrap - Wrap a Tx as a Basecoin Tx +func (tx TxDelegate) Wrap() sdk.Tx { return sdk.Tx{tx} } + +// TxUnbond - struct for unbonding transactions +type TxUnbond struct { + PubKey crypto.PubKey `json:"pub_key"` + Shares string `json:"amount"` +} + +// NewTxUnbond - new TxUnbond +func NewTxUnbond(shares string, pubKey crypto.PubKey) sdk.Tx { + return TxUnbond{ + PubKey: pubKey, + Shares: shares, + }.Wrap() +} + +// Wrap - Wrap a Tx as a Basecoin Tx +func (tx TxUnbond) Wrap() sdk.Tx { return sdk.Tx{tx} } + +// ValidateBasic - Check for non-empty candidate, positive shares +func (tx TxUnbond) ValidateBasic() error { + if tx.PubKey.Empty() { + return errCandidateEmpty + } + return nil +} diff --git a/x/stake/tx_test.go b/x/stake/tx_test.go new file mode 100644 index 0000000000..f6d814589d --- /dev/null +++ b/x/stake/tx_test.go @@ -0,0 +1,104 @@ +package stake + +import ( + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/cosmos/cosmos-sdk" + "github.com/cosmos/cosmos-sdk/modules/coin" + + crypto "github.com/tendermint/go-crypto" + wire "github.com/tendermint/go-wire" +) + +var ( + validator = sdk.Actor{"testChain", "testapp", []byte("addressvalidator1")} + empty sdk.Actor + + coinPos = coin.Coin{"fermion", 1000} + coinZero = coin.Coin{"fermion", 0} + coinNeg = coin.Coin{"fermion", -10000} + coinPosNotAtoms = coin.Coin{"foo", 10000} + coinZeroNotAtoms = coin.Coin{"foo", 0} + coinNegNotAtoms = coin.Coin{"foo", -10000} +) + +func TestBondUpdateValidateBasic(t *testing.T) { + tests := []struct { + name string + PubKey crypto.PubKey + Bond coin.Coin + wantErr bool + }{ + {"basic good", pks[0], coinPos, false}, + {"empty delegator", crypto.PubKey{}, coinPos, true}, + {"zero coin", pks[0], coinZero, true}, + {"neg coin", pks[0], coinNeg, true}, + } + + for _, tc := range tests { + tx := TxDelegate{BondUpdate{ + PubKey: tc.PubKey, + Bond: tc.Bond, + }} + assert.Equal(t, tc.wantErr, tx.ValidateBasic() != nil, + "test: %v, tx.ValidateBasic: %v", tc.name, tx.ValidateBasic()) + } +} + +func TestAllAreTx(t *testing.T) { + assert := assert.New(t) + + // make sure all types construct properly + pubKey := newPubKey("1234567890") + bondAmt := 1234321 + bond := coin.Coin{Denom: "ATOM", Amount: int64(bondAmt)} + + // Note that Wrap is only defined on BondUpdate, so when you call it, + // you lose all info on the embedding type. Please add Wrap() + // method to all the parents + txDelegate := NewTxDelegate(bond, pubKey) + _, ok := txDelegate.Unwrap().(TxDelegate) + assert.True(ok, "%#v", txDelegate) + + txUnbond := NewTxUnbond(strconv.Itoa(bondAmt), pubKey) + _, ok = txUnbond.Unwrap().(TxUnbond) + assert.True(ok, "%#v", txUnbond) + + txDecl := NewTxDeclareCandidacy(bond, pubKey, Description{}) + _, ok = txDecl.Unwrap().(TxDeclareCandidacy) + assert.True(ok, "%#v", txDecl) + + txEditCan := NewTxEditCandidacy(pubKey, Description{}) + _, ok = txEditCan.Unwrap().(TxEditCandidacy) + assert.True(ok, "%#v", txEditCan) +} + +func TestSerializeTx(t *testing.T) { + assert := assert.New(t) + + // make sure all types construct properly + pubKey := newPubKey("1234567890") + bondAmt := 1234321 + bond := coin.Coin{Denom: "ATOM", Amount: int64(bondAmt)} + + tests := []struct { + tx sdk.Tx + }{ + {NewTxUnbond(strconv.Itoa(bondAmt), pubKey)}, + {NewTxDeclareCandidacy(bond, pubKey, Description{})}, + {NewTxDeclareCandidacy(bond, pubKey, Description{})}, + // {NewTxRevokeCandidacy(pubKey)}, + } + + for i, tc := range tests { + var tx sdk.Tx + bs := wire.BinaryBytes(tc.tx) + err := wire.ReadBinaryBytes(bs, &tx) + if assert.NoError(err, "%d", i) { + assert.Equal(tc.tx, tx, "%d", i) + } + } +} diff --git a/x/stake/types.go b/x/stake/types.go new file mode 100644 index 0000000000..0ca88ef11d --- /dev/null +++ b/x/stake/types.go @@ -0,0 +1,414 @@ +package stake + +import ( + "bytes" + "sort" + + "github.com/cosmos/cosmos-sdk" + "github.com/cosmos/cosmos-sdk/state" + + abci "github.com/tendermint/abci/types" + crypto "github.com/tendermint/go-crypto" + wire "github.com/tendermint/go-wire" + "github.com/tendermint/tmlibs/rational" +) + +// Params defines the high level settings for staking +type Params struct { + HoldBonded sdk.Actor `json:"hold_bonded"` // account where all bonded coins are held + HoldUnbonded sdk.Actor `json:"hold_unbonded"` // account where all delegated but unbonded coins are held + + InflationRateChange rational.Rat `json:"inflation_rate_change"` // maximum annual change in inflation rate + InflationMax rational.Rat `json:"inflation_max"` // maximum inflation rate + InflationMin rational.Rat `json:"inflation_min"` // minimum inflation rate + GoalBonded rational.Rat `json:"goal_bonded"` // Goal of percent bonded atoms + + MaxVals uint16 `json:"max_vals"` // maximum number of validators + AllowedBondDenom string `json:"allowed_bond_denom"` // bondable coin denomination + + // gas costs for txs + GasDeclareCandidacy int64 `json:"gas_declare_candidacy"` + GasEditCandidacy int64 `json:"gas_edit_candidacy"` + GasDelegate int64 `json:"gas_delegate"` + GasUnbond int64 `json:"gas_unbond"` +} + +func defaultParams() Params { + return Params{ + HoldBonded: sdk.NewActor(stakingModuleName, []byte("77777777777777777777777777777777")), + HoldUnbonded: sdk.NewActor(stakingModuleName, []byte("88888888888888888888888888888888")), + InflationRateChange: rational.New(13, 100), + InflationMax: rational.New(20, 100), + InflationMin: rational.New(7, 100), + GoalBonded: rational.New(67, 100), + MaxVals: 100, + AllowedBondDenom: "fermion", + GasDeclareCandidacy: 20, + GasEditCandidacy: 20, + GasDelegate: 20, + GasUnbond: 20, + } +} + +//_________________________________________________________________________ + +// GlobalState - dynamic parameters of the current state +type GlobalState struct { + TotalSupply int64 `json:"total_supply"` // total supply of all tokens + BondedShares rational.Rat `json:"bonded_shares"` // sum of all shares distributed for the Bonded Pool + UnbondedShares rational.Rat `json:"unbonded_shares"` // sum of all shares distributed for the Unbonded Pool + BondedPool int64 `json:"bonded_pool"` // reserve of bonded tokens + UnbondedPool int64 `json:"unbonded_pool"` // reserve of unbonded tokens held with candidates + InflationLastTime int64 `json:"inflation_last_time"` // block which the last inflation was processed // TODO make time + Inflation rational.Rat `json:"inflation"` // current annual inflation rate +} + +// XXX define globalstate interface? + +func initialGlobalState() *GlobalState { + return &GlobalState{ + TotalSupply: 0, + BondedShares: rational.Zero, + UnbondedShares: rational.Zero, + BondedPool: 0, + UnbondedPool: 0, + InflationLastTime: 0, + Inflation: rational.New(7, 100), + } +} + +// get the bond ratio of the global state +func (gs *GlobalState) bondedRatio() rational.Rat { + if gs.TotalSupply > 0 { + return rational.New(gs.BondedPool, gs.TotalSupply) + } + return rational.Zero +} + +// get the exchange rate of bonded token per issued share +func (gs *GlobalState) bondedShareExRate() rational.Rat { + if gs.BondedShares.IsZero() { + return rational.One + } + return gs.BondedShares.Inv().Mul(rational.New(gs.BondedPool)) +} + +// get the exchange rate of unbonded tokens held in candidates per issued share +func (gs *GlobalState) unbondedShareExRate() rational.Rat { + if gs.UnbondedShares.IsZero() { + return rational.One + } + return gs.UnbondedShares.Inv().Mul(rational.New(gs.UnbondedPool)) +} + +func (gs *GlobalState) addTokensBonded(amount int64) (issuedShares rational.Rat) { + issuedShares = gs.bondedShareExRate().Inv().Mul(rational.New(amount)) // (tokens/shares)^-1 * tokens + gs.BondedPool += amount + gs.BondedShares = gs.BondedShares.Add(issuedShares) + return +} + +func (gs *GlobalState) removeSharesBonded(shares rational.Rat) (removedTokens int64) { + removedTokens = gs.bondedShareExRate().Mul(shares).Evaluate() // (tokens/shares) * shares + gs.BondedShares = gs.BondedShares.Sub(shares) + gs.BondedPool -= removedTokens + return +} + +func (gs *GlobalState) addTokensUnbonded(amount int64) (issuedShares rational.Rat) { + issuedShares = gs.unbondedShareExRate().Inv().Mul(rational.New(amount)) // (tokens/shares)^-1 * tokens + gs.UnbondedShares = gs.UnbondedShares.Add(issuedShares) + gs.UnbondedPool += amount + return +} + +func (gs *GlobalState) removeSharesUnbonded(shares rational.Rat) (removedTokens int64) { + removedTokens = gs.unbondedShareExRate().Mul(shares).Evaluate() // (tokens/shares) * shares + gs.UnbondedShares = gs.UnbondedShares.Sub(shares) + gs.UnbondedPool -= removedTokens + return +} + +//_______________________________________________________________________________________________________ + +// CandidateStatus - status of a validator-candidate +type CandidateStatus byte + +const ( + // nolint + Bonded CandidateStatus = 0x00 + Unbonded CandidateStatus = 0x01 + Revoked CandidateStatus = 0x02 +) + +// Candidate defines the total amount of bond shares and their exchange rate to +// coins. Accumulation of interest is modelled as an in increase in the +// exchange rate, and slashing as a decrease. When coins are delegated to this +// candidate, the candidate is credited with a DelegatorBond whose number of +// bond shares is based on the amount of coins delegated divided by the current +// exchange rate. Voting power can be calculated as total bonds multiplied by +// exchange rate. +type Candidate struct { + Status CandidateStatus `json:"status"` // Bonded status + PubKey crypto.PubKey `json:"pub_key"` // Pubkey of candidate + Owner sdk.Actor `json:"owner"` // Sender of BondTx - UnbondTx returns here + Assets rational.Rat `json:"assets"` // total shares of a global hold pools TODO custom type PoolShares + Liabilities rational.Rat `json:"liabilities"` // total shares issued to a candidate's delegators TODO custom type DelegatorShares + VotingPower rational.Rat `json:"voting_power"` // Voting power if considered a validator + Description Description `json:"description"` // Description terms for the candidate +} + +// Description - description fields for a candidate +type Description struct { + Moniker string `json:"moniker"` + Identity string `json:"identity"` + Website string `json:"website"` + Details string `json:"details"` +} + +// NewCandidate - initialize a new candidate +func NewCandidate(pubKey crypto.PubKey, owner sdk.Actor, description Description) *Candidate { + return &Candidate{ + Status: Unbonded, + PubKey: pubKey, + Owner: owner, + Assets: rational.Zero, + Liabilities: rational.Zero, + VotingPower: rational.Zero, + Description: description, + } +} + +// XXX define candidate interface? + +// get the exchange rate of global pool shares over delegator shares +func (c *Candidate) delegatorShareExRate() rational.Rat { + if c.Liabilities.IsZero() { + return rational.One + } + return c.Assets.Quo(c.Liabilities) +} + +// add tokens to a candidate +func (c *Candidate) addTokens(amount int64, gs *GlobalState) (issuedDelegatorShares rational.Rat) { + + exRate := c.delegatorShareExRate() + + var receivedGlobalShares rational.Rat + if c.Status == Bonded { + receivedGlobalShares = gs.addTokensBonded(amount) + } else { + receivedGlobalShares = gs.addTokensUnbonded(amount) + } + c.Assets = c.Assets.Add(receivedGlobalShares) + + issuedDelegatorShares = exRate.Mul(receivedGlobalShares) + c.Liabilities = c.Liabilities.Add(issuedDelegatorShares) + return +} + +// remove shares from a candidate +func (c *Candidate) removeShares(shares rational.Rat, gs *GlobalState) (removedTokens int64) { + + globalPoolSharesToRemove := c.delegatorShareExRate().Mul(shares) + + if c.Status == Bonded { + removedTokens = gs.removeSharesBonded(globalPoolSharesToRemove) + } else { + removedTokens = gs.removeSharesUnbonded(globalPoolSharesToRemove) + } + c.Assets = c.Assets.Sub(globalPoolSharesToRemove) + + c.Liabilities = c.Liabilities.Sub(shares) + return +} + +// Validator returns a copy of the Candidate as a Validator. +// Should only be called when the Candidate qualifies as a validator. +func (c *Candidate) validator() Validator { + return Validator(*c) +} + +// Validator is one of the top Candidates +type Validator Candidate + +// ABCIValidator - Get the validator from a bond value +func (v Validator) ABCIValidator() *abci.Validator { + return &abci.Validator{ + PubKey: wire.BinaryBytes(v.PubKey), + Power: v.VotingPower.Evaluate(), + } +} + +//_________________________________________________________________________ + +// TODO replace with sorted multistore functionality + +// Candidates - list of Candidates +type Candidates []*Candidate + +var _ sort.Interface = Candidates{} //enforce the sort interface at compile time + +// nolint - sort interface functions +func (cs Candidates) Len() int { return len(cs) } +func (cs Candidates) Swap(i, j int) { cs[i], cs[j] = cs[j], cs[i] } +func (cs Candidates) Less(i, j int) bool { + vp1, vp2 := cs[i].VotingPower, cs[j].VotingPower + pk1, pk2 := cs[i].PubKey.Bytes(), cs[j].PubKey.Bytes() + + //note that all ChainId and App must be the same for a group of candidates + if vp1 != vp2 { + return vp1.GT(vp2) + } + return bytes.Compare(pk1, pk2) == -1 +} + +// Sort - Sort the array of bonded values +func (cs Candidates) Sort() { + sort.Sort(cs) +} + +// update the voting power and save +func (cs Candidates) updateVotingPower(store state.SimpleDB, gs *GlobalState, params Params) Candidates { + + // update voting power + for _, c := range cs { + if !c.VotingPower.Equal(c.Assets) { + c.VotingPower = c.Assets + } + } + cs.Sort() + for i, c := range cs { + // truncate the power + if i >= int(params.MaxVals) { + c.VotingPower = rational.Zero + if c.Status == Bonded { + // XXX to replace this with handler.bondedToUnbondePool function + // XXX waiting for logic with new SDK to update account balance here + tokens := gs.removeSharesBonded(c.Assets) + c.Assets = gs.addTokensUnbonded(tokens) + c.Status = Unbonded + } + } else { + c.Status = Bonded + } + saveCandidate(store, c) + } + return cs +} + +// Validators - get the most recent updated validator set from the +// Candidates. These bonds are already sorted by VotingPower from +// the UpdateVotingPower function which is the only function which +// is to modify the VotingPower +func (cs Candidates) Validators() Validators { + + //test if empty + if len(cs) == 1 { + if cs[0].VotingPower.IsZero() { + return nil + } + } + + validators := make(Validators, len(cs)) + for i, c := range cs { + if c.VotingPower.IsZero() { //exit as soon as the first Voting power set to zero is found + return validators[:i] + } + validators[i] = c.validator() + } + + return validators +} + +//_________________________________________________________________________ + +// Validators - list of Validators +type Validators []Validator + +var _ sort.Interface = Validators{} //enforce the sort interface at compile time + +// nolint - sort interface functions +func (vs Validators) Len() int { return len(vs) } +func (vs Validators) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] } +func (vs Validators) Less(i, j int) bool { + pk1, pk2 := vs[i].PubKey.Bytes(), vs[j].PubKey.Bytes() + return bytes.Compare(pk1, pk2) == -1 +} + +// Sort - Sort validators by pubkey +func (vs Validators) Sort() { + sort.Sort(vs) +} + +// determine all updated validators between two validator sets +func (vs Validators) validatorsUpdated(vs2 Validators) (updated []*abci.Validator) { + + //first sort the validator sets + vs.Sort() + vs2.Sort() + + max := len(vs) + len(vs2) + updated = make([]*abci.Validator, max) + i, j, n := 0, 0, 0 //counters for vs loop, vs2 loop, updated element + + for i < len(vs) && j < len(vs2) { + + if !vs[i].PubKey.Equals(vs2[j].PubKey) { + // pk1 > pk2, a new validator was introduced between these pubkeys + if bytes.Compare(vs[i].PubKey.Bytes(), vs2[j].PubKey.Bytes()) == 1 { + updated[n] = vs2[j].ABCIValidator() + n++ + j++ + continue + } // else, the old validator has been removed + updated[n] = &abci.Validator{vs[i].PubKey.Bytes(), 0} + n++ + i++ + continue + } + + if vs[i].VotingPower != vs2[j].VotingPower { + updated[n] = vs2[j].ABCIValidator() + n++ + } + j++ + i++ + } + + // add any excess validators in set 2 + for ; j < len(vs2); j, n = j+1, n+1 { + updated[n] = vs2[j].ABCIValidator() + } + + // remove any excess validators left in set 1 + for ; i < len(vs); i, n = i+1, n+1 { + updated[n] = &abci.Validator{vs[i].PubKey.Bytes(), 0} + } + + return updated[:n] +} + +// UpdateValidatorSet - Updates the voting power for the candidate set and +// returns the subset of validators which have been updated for Tendermint +func UpdateValidatorSet(store state.SimpleDB, gs *GlobalState, params Params) (change []*abci.Validator, err error) { + + // get the validators before update + candidates := loadCandidates(store) + + v1 := candidates.Validators() + v2 := candidates.updateVotingPower(store, gs, params).Validators() + + change = v1.validatorsUpdated(v2) + return +} + +//_________________________________________________________________________ + +// DelegatorBond represents the bond with tokens held by an account. It is +// owned by one delegator, and is associated with the voting power of one +// pubKey. +type DelegatorBond struct { + PubKey crypto.PubKey `json:"pub_key"` + Shares rational.Rat `json:"shares"` +} diff --git a/x/stake/types_test.go b/x/stake/types_test.go new file mode 100644 index 0000000000..d78df60b00 --- /dev/null +++ b/x/stake/types_test.go @@ -0,0 +1,251 @@ +package stake + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tendermint/tmlibs/rational" + + "github.com/cosmos/cosmos-sdk/state" +) + +func TestCandidatesSort(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + N := 5 + actors := newActors(N) + candidates := candidatesFromActors(actors, []int64{10, 300, 123, 4, 200}) + expectedOrder := []int{1, 4, 2, 0, 3} + + // test basic sort + candidates.Sort() + + vals := candidates.Validators() + require.Equal(N, len(vals)) + + for i, val := range vals { + expectedIdx := expectedOrder[i] + assert.Equal(val.PubKey, pks[expectedIdx]) + } +} + +func TestValidatorsSort(t *testing.T) { + assert := assert.New(t) + + v1 := (&Candidate{PubKey: pks[0], VotingPower: rational.New(25)}).validator() + v2 := (&Candidate{PubKey: pks[1], VotingPower: rational.New(1234)}).validator() + v3 := (&Candidate{PubKey: pks[2], VotingPower: rational.New(122)}).validator() + v4 := (&Candidate{PubKey: pks[3], VotingPower: rational.New(13)}).validator() + v5 := (&Candidate{PubKey: pks[4], VotingPower: rational.New(1111)}).validator() + + // test from nothing to something + vs := Validators{v4, v2, v5, v1, v3} + + // test basic sort + vs.Sort() + + for i, v := range vs { + assert.True(v.PubKey.Equals(pks[i])) + } +} + +func TestUpdateVotingPower(t *testing.T) { + assert := assert.New(t) + store := state.NewMemKVStore() + params := loadParams(store) + gs := loadGlobalState(store) + + N := 5 + actors := newActors(N) + candidates := candidatesFromActors(actors, []int64{400, 200, 100, 10, 1}) + + // test a basic change in voting power + candidates[0].Assets = rational.New(500) + candidates.updateVotingPower(store, gs, params) + assert.Equal(int64(500), candidates[0].VotingPower.Evaluate(), "%v", candidates[0]) + + // test a swap in voting power + candidates[1].Assets = rational.New(600) + candidates.updateVotingPower(store, gs, params) + assert.Equal(int64(600), candidates[0].VotingPower.Evaluate(), "%v", candidates[0]) + assert.Equal(int64(500), candidates[1].VotingPower.Evaluate(), "%v", candidates[1]) + + // test the max validators term + params.MaxVals = 4 + saveParams(store, params) + candidates.updateVotingPower(store, gs, params) + assert.Equal(int64(0), candidates[4].VotingPower.Evaluate(), "%v", candidates[4]) +} + +func TestGetValidators(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + N := 5 + actors := newActors(N) + candidates := candidatesFromActors(actors, []int64{400, 200, 0, 0, 0}) + + validators := candidates.Validators() + require.Equal(2, len(validators)) + assert.Equal(candidates[0].PubKey, validators[0].PubKey) + assert.Equal(candidates[1].PubKey, validators[1].PubKey) +} + +func TestValidatorsChanged(t *testing.T) { + require := require.New(t) + + v1 := (&Candidate{PubKey: pks[0], VotingPower: rational.New(10)}).validator() + v2 := (&Candidate{PubKey: pks[1], VotingPower: rational.New(10)}).validator() + v3 := (&Candidate{PubKey: pks[2], VotingPower: rational.New(10)}).validator() + v4 := (&Candidate{PubKey: pks[3], VotingPower: rational.New(10)}).validator() + v5 := (&Candidate{PubKey: pks[4], VotingPower: rational.New(10)}).validator() + + // test from nothing to something + vs1 := Validators{} + vs2 := Validators{v1, v2} + changed := vs1.validatorsUpdated(vs2) + require.Equal(2, len(changed)) + testChange(t, vs2[0], changed[0]) + testChange(t, vs2[1], changed[1]) + + // test from something to nothing + vs1 = Validators{v1, v2} + vs2 = Validators{} + changed = vs1.validatorsUpdated(vs2) + require.Equal(2, len(changed)) + testRemove(t, vs1[0], changed[0]) + testRemove(t, vs1[1], changed[1]) + + // test identical + vs1 = Validators{v1, v2, v4} + vs2 = Validators{v1, v2, v4} + changed = vs1.validatorsUpdated(vs2) + require.Zero(len(changed)) + + // test single value change + vs2[2].VotingPower = rational.One + changed = vs1.validatorsUpdated(vs2) + require.Equal(1, len(changed)) + testChange(t, vs2[2], changed[0]) + + // test multiple value change + vs2[0].VotingPower = rational.New(11) + vs2[2].VotingPower = rational.New(5) + changed = vs1.validatorsUpdated(vs2) + require.Equal(2, len(changed)) + testChange(t, vs2[0], changed[0]) + testChange(t, vs2[2], changed[1]) + + // test validator added at the beginning + vs1 = Validators{v2, v4} + vs2 = Validators{v2, v4, v1} + changed = vs1.validatorsUpdated(vs2) + require.Equal(1, len(changed)) + testChange(t, vs2[0], changed[0]) + + // test validator added in the middle + vs1 = Validators{v1, v2, v4} + vs2 = Validators{v3, v1, v4, v2} + changed = vs1.validatorsUpdated(vs2) + require.Equal(1, len(changed)) + testChange(t, vs2[2], changed[0]) + + // test validator added at the end + vs2 = Validators{v1, v2, v4, v5} + changed = vs1.validatorsUpdated(vs2) + require.Equal(1, len(changed)) + testChange(t, vs2[3], changed[0]) + + // test multiple validators added + vs2 = Validators{v1, v2, v3, v4, v5} + changed = vs1.validatorsUpdated(vs2) + require.Equal(2, len(changed)) + testChange(t, vs2[2], changed[0]) + testChange(t, vs2[4], changed[1]) + + // test validator removed at the beginning + vs2 = Validators{v2, v4} + changed = vs1.validatorsUpdated(vs2) + require.Equal(1, len(changed)) + testRemove(t, vs1[0], changed[0]) + + // test validator removed in the middle + vs2 = Validators{v1, v4} + changed = vs1.validatorsUpdated(vs2) + require.Equal(1, len(changed)) + testRemove(t, vs1[1], changed[0]) + + // test validator removed at the end + vs2 = Validators{v1, v2} + changed = vs1.validatorsUpdated(vs2) + require.Equal(1, len(changed)) + testRemove(t, vs1[2], changed[0]) + + // test multiple validators removed + vs2 = Validators{v1} + changed = vs1.validatorsUpdated(vs2) + require.Equal(2, len(changed)) + testRemove(t, vs1[1], changed[0]) + testRemove(t, vs1[2], changed[1]) + + // test many types of changes + vs2 = Validators{v1, v3, v4, v5} + vs2[2].VotingPower = rational.New(11) + changed = vs1.validatorsUpdated(vs2) + require.Equal(4, len(changed), "%v", changed) // change 1, remove 1, add 2 + testRemove(t, vs1[1], changed[0]) + testChange(t, vs2[1], changed[1]) + testChange(t, vs2[2], changed[2]) + testChange(t, vs2[3], changed[3]) + +} + +func TestUpdateValidatorSet(t *testing.T) { + assert, require := assert.New(t), require.New(t) + store := state.NewMemKVStore() + params := loadParams(store) + gs := loadGlobalState(store) + + N := 5 + actors := newActors(N) + candidates := candidatesFromActors(actors, []int64{400, 200, 100, 10, 1}) + for _, c := range candidates { + saveCandidate(store, c) + } + + // they should all already be validators + change, err := UpdateValidatorSet(store, gs, params) + require.Nil(err) + require.Equal(0, len(change), "%v", change) // change 1, remove 1, add 2 + + // test the max value and test again + params.MaxVals = 4 + saveParams(store, params) + change, err = UpdateValidatorSet(store, gs, params) + require.Nil(err) + require.Equal(1, len(change), "%v", change) + testRemove(t, candidates[4].validator(), change[0]) + candidates = loadCandidates(store) + assert.Equal(int64(0), candidates[4].VotingPower.Evaluate()) + + // mess with the power's of the candidates and test + candidates[0].Assets = rational.New(10) + candidates[1].Assets = rational.New(600) + candidates[2].Assets = rational.New(1000) + candidates[3].Assets = rational.One + candidates[4].Assets = rational.New(10) + for _, c := range candidates { + saveCandidate(store, c) + } + change, err = UpdateValidatorSet(store, gs, params) + require.Nil(err) + require.Equal(5, len(change), "%v", change) // 3 changed, 1 added, 1 removed + candidates = loadCandidates(store) + testChange(t, candidates[0].validator(), change[0]) + testChange(t, candidates[1].validator(), change[1]) + testChange(t, candidates[2].validator(), change[2]) + testRemove(t, candidates[3].validator(), change[3]) + testChange(t, candidates[4].validator(), change[4]) +} + +// XXX test global state functions, candidate exchange rate functions etc.