Implement Simple Staking as a module

The simple staking module allows validators to bond and add more stake
to their bond. It doesn't allow partial unbond and has no delegation.
The staking power per validator though is correctly reflected within the
consensus.
This commit is contained in:
Adrian Brink 2018-03-18 22:37:10 +01:00
parent 4bfa40adbd
commit 75674a9ec3
No known key found for this signature in database
GPG Key ID: F61053D3FBD06353
11 changed files with 446 additions and 16 deletions

3
.gitignore vendored
View File

@ -14,6 +14,9 @@ docs/_build
coverage.txt
profile.out
.vscode
coverage.txt
profile.out
client/lcd/keys.db/
### Vagrant ###
.vagrant/

View File

@ -71,7 +71,7 @@ test_unit:
@go test $(PACKAGES)
test_cover:
@rm -rf examples/basecoin/vendor
@rm -rf examples/basecoin/vendor/
@rm -rf client/lcd/keys.db ~/.tendermint_test
@bash tests/test_cover.sh
@rm -rf client/lcd/keys.db ~/.tendermint_test

View File

@ -15,6 +15,7 @@ import (
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/bank"
"github.com/cosmos/cosmos-sdk/x/ibc"
"github.com/cosmos/cosmos-sdk/x/staking"
"github.com/cosmos/cosmos-sdk/examples/basecoin/types"
"github.com/cosmos/cosmos-sdk/examples/basecoin/x/cool"
@ -31,8 +32,9 @@ type BasecoinApp struct {
cdc *wire.Codec
// keys to access the substores
capKeyMainStore *sdk.KVStoreKey
capKeyIBCStore *sdk.KVStoreKey
capKeyMainStore *sdk.KVStoreKey
capKeyIBCStore *sdk.KVStoreKey
capKeyStakingStore *sdk.KVStoreKey
// Manage getting and setting accounts
accountMapper sdk.AccountMapper
@ -41,10 +43,11 @@ type BasecoinApp struct {
func NewBasecoinApp(logger log.Logger, db dbm.DB) *BasecoinApp {
// create your application object
var app = &BasecoinApp{
BaseApp: bam.NewBaseApp(appName, logger, db),
cdc: MakeCodec(),
capKeyMainStore: sdk.NewKVStoreKey("main"),
capKeyIBCStore: sdk.NewKVStoreKey("ibc"),
BaseApp: bam.NewBaseApp(appName, logger, db),
cdc: MakeCodec(),
capKeyMainStore: sdk.NewKVStoreKey("main"),
capKeyIBCStore: sdk.NewKVStoreKey("ibc"),
capKeyStakingStore: sdk.NewKVStoreKey("staking"),
}
// define the accountMapper
@ -57,18 +60,18 @@ func NewBasecoinApp(logger log.Logger, db dbm.DB) *BasecoinApp {
coinKeeper := bank.NewCoinKeeper(app.accountMapper)
coolMapper := cool.NewMapper(app.capKeyMainStore)
ibcMapper := ibc.NewIBCMapper(app.cdc, app.capKeyIBCStore)
stakingMapper := staking.NewMapper(app.capKeyStakingStore)
app.Router().
AddRoute("bank", bank.NewHandler(coinKeeper)).
AddRoute("cool", cool.NewHandler(coinKeeper, coolMapper)).
AddRoute("sketchy", sketchy.NewHandler()).
AddRoute("ibc", ibc.NewHandler(ibcMapper, coinKeeper))
AddRoute("ibc", ibc.NewHandler(ibcMapper, coinKeeper)).
AddRoute("staking", staking.NewHandler(stakingMapper, coinKeeper))
// initialize BaseApp
app.SetTxDecoder(app.txDecoder)
app.SetInitChainer(app.initChainer)
// TODO: mounting multiple stores is broken
// https://github.com/cosmos/cosmos-sdk/issues/532
app.MountStoresIAVL(app.capKeyMainStore, app.capKeyIBCStore)
app.MountStoresIAVL(app.capKeyMainStore, app.capKeyIBCStore, app.capKeyStakingStore)
app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper))
err := app.LoadLatestVersion(app.capKeyMainStore)
if err != nil {
@ -81,13 +84,14 @@ func NewBasecoinApp(logger log.Logger, db dbm.DB) *BasecoinApp {
// custom tx codec
// TODO: use new go-wire
func MakeCodec() *wire.Codec {
const msgTypeSend = 0x1
const msgTypeIssue = 0x2
const msgTypeQuiz = 0x3
const msgTypeSetTrend = 0x4
const msgTypeIBCTransferMsg = 0x5
const msgTypeIBCReceiveMsg = 0x6
const msgTypeBondMsg = 0x7
const msgTypeUnbondMsg = 0x8
var _ = oldwire.RegisterInterface(
struct{ sdk.Msg }{},
oldwire.ConcreteType{bank.SendMsg{}, msgTypeSend},
@ -96,6 +100,8 @@ func MakeCodec() *wire.Codec {
oldwire.ConcreteType{cool.SetTrendMsg{}, msgTypeSetTrend},
oldwire.ConcreteType{ibc.IBCTransferMsg{}, msgTypeIBCTransferMsg},
oldwire.ConcreteType{ibc.IBCReceiveMsg{}, msgTypeIBCReceiveMsg},
oldwire.ConcreteType{staking.BondMsg{}, msgTypeBondMsg},
oldwire.ConcreteType{staking.UnbondMsg{}, msgTypeUnbondMsg},
)
const accTypeApp = 0x1

View File

@ -2,9 +2,8 @@ package main
import (
"errors"
"os"
"github.com/spf13/cobra"
"os"
"github.com/tendermint/tmlibs/cli"
@ -14,14 +13,15 @@ import (
"github.com/cosmos/cosmos-sdk/client/rpc"
"github.com/cosmos/cosmos-sdk/client/tx"
coolcmd "github.com/cosmos/cosmos-sdk/examples/basecoin/x/cool/commands"
"github.com/cosmos/cosmos-sdk/version"
authcmd "github.com/cosmos/cosmos-sdk/x/auth/commands"
bankcmd "github.com/cosmos/cosmos-sdk/x/bank/commands"
ibccmd "github.com/cosmos/cosmos-sdk/x/ibc/commands"
stakingcmd "github.com/cosmos/cosmos-sdk/x/staking/commands"
"github.com/cosmos/cosmos-sdk/examples/basecoin/app"
"github.com/cosmos/cosmos-sdk/examples/basecoin/types"
coolcmd "github.com/cosmos/cosmos-sdk/examples/basecoin/x/cool/commands"
)
// gaiacliCmd is the entry point for this binary
@ -77,6 +77,11 @@ func main() {
basecliCmd.AddCommand(
client.PostCommands(
ibccmd.IBCRelayCmd(cdc),
stakingcmd.BondTxCmd(cdc),
)...)
basecliCmd.AddCommand(
client.PostCommands(
stakingcmd.UnbondTxCmd(cdc),
)...)
// add proxy, version and key info

View File

@ -1,7 +1,7 @@
package server
import (
//"os"
// "os"
"testing"
"time"

View File

@ -0,0 +1,100 @@
package commands
import (
"encoding/hex"
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
crypto "github.com/tendermint/go-crypto"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/builder"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/wire"
"github.com/cosmos/cosmos-sdk/x/staking"
)
const (
flagStake = "stake"
flagValidator = "validator"
)
func BondTxCmd(cdc *wire.Codec) *cobra.Command {
cmdr := commander{cdc}
cmd := &cobra.Command{
Use: "bond",
Short: "Bond to a validator",
RunE: cmdr.bondTxCmd,
}
cmd.Flags().String(flagStake, "", "Amount of coins to stake")
cmd.Flags().String(flagValidator, "", "Validator address to stake")
return cmd
}
func UnbondTxCmd(cdc *wire.Codec) *cobra.Command {
cmdr := commander{cdc}
cmd := &cobra.Command{
Use: "unbond",
Short: "Unbond from a validator",
RunE: cmdr.unbondTxCmd,
}
return cmd
}
type commander struct {
cdc *wire.Codec
}
func (co commander) bondTxCmd(cmd *cobra.Command, args []string) error {
from, err := builder.GetFromAddress()
if err != nil {
return err
}
stake, err := sdk.ParseCoin(viper.GetString(flagStake))
if err != nil {
return err
}
rawPubKey, err := hex.DecodeString(viper.GetString(flagValidator))
if err != nil {
return err
}
var pubKey crypto.PubKeyEd25519
copy(pubKey[:], rawPubKey)
msg := staking.NewBondMsg(from, stake, pubKey.Wrap())
return co.sendMsg(msg)
}
func (co commander) unbondTxCmd(cmd *cobra.Command, args []string) error {
from, err := builder.GetFromAddress()
if err != nil {
return err
}
msg := staking.NewUnbondMsg(from)
return co.sendMsg(msg)
}
func (co commander) sendMsg(msg sdk.Msg) error {
name := viper.GetString(client.FlagName)
buf := client.BufferStdin()
prompt := fmt.Sprintf("Password to sign with '%s':", name)
passphrase, err := client.GetPassword(prompt, buf)
if err != nil {
return err
}
res, err := builder.SignBuildBroadcast(name, passphrase, msg, co.cdc)
if err != nil {
return err
}
fmt.Printf("Committed at block %d. Hash: %s\n", res.Height, res.Hash.String())
return nil
}

26
x/staking/errors.go Normal file
View File

@ -0,0 +1,26 @@
package staking
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
// Staking errors reserve 300 - 399.
CodeEmptyValidator sdk.CodeType = 300
CodeInvalidUnbond sdk.CodeType = 301
)
func ErrEmptyValidator() sdk.Error {
return newError(CodeEmptyValidator, "")
}
func ErrInvalidUnbond() sdk.Error {
return newError(CodeInvalidUnbond, "")
}
// -----------------------------
// Helpers
func newError(code sdk.CodeType, msg string) sdk.Error {
return sdk.NewError(code, msg)
}

69
x/staking/handler.go Normal file
View File

@ -0,0 +1,69 @@
package staking
import (
abci "github.com/tendermint/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/bank"
)
func NewHandler(sm StakingMapper, ck bank.CoinKeeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
switch msg := msg.(type) {
case BondMsg:
return handleBondMsg(ctx, sm, ck, msg)
case UnbondMsg:
return handleUnbondMsg(ctx, sm, ck, msg)
default:
return sdk.ErrUnknownRequest("No match for message type.").Result()
}
}
}
func handleBondMsg(ctx sdk.Context, sm StakingMapper, ck bank.CoinKeeper, msg BondMsg) sdk.Result {
_, err := ck.SubtractCoins(ctx, msg.Address, []sdk.Coin{msg.Stake})
if err != nil {
return err.Result()
}
power, err := sm.Bond(ctx, msg.Address, msg.PubKey, msg.Stake.Amount)
if err != nil {
return err.Result()
}
valSet := abci.Validator{
PubKey: msg.PubKey.Bytes(),
Power: power,
}
return sdk.Result{
Code: sdk.CodeOK,
ValidatorUpdates: abci.Validators{valSet},
}
}
func handleUnbondMsg(ctx sdk.Context, sm StakingMapper, ck bank.CoinKeeper, msg UnbondMsg) sdk.Result {
pubKey, power, err := sm.Unbond(ctx, msg.Address)
if err != nil {
return err.Result()
}
stake := sdk.Coin{
Denom: "mycoin",
Amount: power,
}
_, err = ck.AddCoins(ctx, msg.Address, sdk.Coins{stake})
if err != nil {
return err.Result()
}
valSet := abci.Validator{
PubKey: pubKey.Bytes(),
Power: int64(0),
}
return sdk.Result{
Code: sdk.CodeOK,
ValidatorUpdates: abci.Validators{valSet},
}
}

84
x/staking/mapper.go Normal file
View File

@ -0,0 +1,84 @@
package staking
import (
crypto "github.com/tendermint/go-crypto"
sdk "github.com/cosmos/cosmos-sdk/types"
wire "github.com/cosmos/cosmos-sdk/wire"
)
type StakingMapper struct {
key sdk.StoreKey
cdc *wire.Codec
}
func NewMapper(key sdk.StoreKey) StakingMapper {
cdc := wire.NewCodec()
return StakingMapper{
key: key,
cdc: cdc,
}
}
func (sm StakingMapper) getBondInfo(ctx sdk.Context, addr sdk.Address) *bondInfo {
store := ctx.KVStore(sm.key)
bz := store.Get(addr)
if bz == nil {
return nil
}
var bi bondInfo
err := sm.cdc.UnmarshalBinary(bz, &bi)
if err != nil {
panic(err)
}
return &bi
}
func (sm StakingMapper) setBondInfo(ctx sdk.Context, addr sdk.Address, bi *bondInfo) {
store := ctx.KVStore(sm.key)
bz, err := sm.cdc.MarshalBinary(*bi)
if err != nil {
panic(err)
}
store.Set(addr, bz)
}
func (sm StakingMapper) deleteBondInfo(ctx sdk.Context, addr sdk.Address) {
store := ctx.KVStore(sm.key)
store.Delete(addr)
}
func (sm StakingMapper) Bond(ctx sdk.Context, addr sdk.Address, pubKey crypto.PubKey, power int64) (int64, sdk.Error) {
bi := sm.getBondInfo(ctx, addr)
if bi == nil {
bi = &bondInfo{
PubKey: pubKey,
Power: power,
}
sm.setBondInfo(ctx, addr, bi)
return bi.Power, nil
}
newPower := bi.Power + power
newBi := &bondInfo{
PubKey: bi.PubKey,
Power: newPower,
}
sm.setBondInfo(ctx, addr, newBi)
return newBi.Power, nil
}
func (sm StakingMapper) Unbond(ctx sdk.Context, addr sdk.Address) (crypto.PubKey, int64, sdk.Error) {
bi := sm.getBondInfo(ctx, addr)
if bi == nil {
return crypto.PubKey{}, 0, ErrInvalidUnbond()
}
sm.deleteBondInfo(ctx, addr)
return bi.PubKey, bi.Power, nil
}
type bondInfo struct {
PubKey crypto.PubKey
Power int64
}

50
x/staking/mapper_test.go Normal file
View File

@ -0,0 +1,50 @@
package staking
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
abci "github.com/tendermint/abci/types"
crypto "github.com/tendermint/go-crypto"
dbm "github.com/tendermint/tmlibs/db"
"github.com/cosmos/cosmos-sdk/store"
sdk "github.com/cosmos/cosmos-sdk/types"
)
func setupMultiStore() (sdk.MultiStore, *sdk.KVStoreKey) {
db := dbm.NewMemDB()
capKey := sdk.NewKVStoreKey("capkey")
ms := store.NewCommitMultiStore(db)
ms.MountStoreWithDB(capKey, sdk.StoreTypeIAVL, db)
ms.LoadLatestVersion()
return ms, capKey
}
func TestStakingMapperGetSet(t *testing.T) {
ms, capKey := setupMultiStore()
ctx := sdk.NewContext(ms, abci.Header{}, false, nil)
stakingMapper := NewMapper(capKey)
addr := sdk.Address([]byte("some-address"))
bi := stakingMapper.getBondInfo(ctx, addr)
assert.Nil(t, bi)
privKey := crypto.GenPrivKeyEd25519()
bi = &bondInfo{
PubKey: privKey.PubKey(),
Power: int64(10),
}
fmt.Printf("Pubkey: %v\n", privKey.PubKey())
stakingMapper.setBondInfo(ctx, addr, bi)
savedBi := stakingMapper.getBondInfo(ctx, addr)
assert.NotNil(t, savedBi)
fmt.Printf("Bond Info: %v\n", savedBi)
assert.Equal(t, int64(10), savedBi.Power)
}

87
x/staking/types.go Normal file
View File

@ -0,0 +1,87 @@
package staking
import (
"encoding/json"
crypto "github.com/tendermint/go-crypto"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// -------------------------
// BondMsg
type BondMsg struct {
Address sdk.Address `json:"address"`
Stake sdk.Coin `json:"coins"`
PubKey crypto.PubKey `json:"pub_key"`
}
func NewBondMsg(addr sdk.Address, stake sdk.Coin, pubKey crypto.PubKey) BondMsg {
return BondMsg{
Address: addr,
Stake: stake,
PubKey: pubKey,
}
}
func (msg BondMsg) Type() string {
return "staking"
}
func (msg BondMsg) ValidateBasic() sdk.Error {
return nil
}
func (msg BondMsg) Get(key interface{}) interface{} {
return nil
}
func (msg BondMsg) GetSignBytes() []byte {
bz, err := json.Marshal(msg)
if err != nil {
panic(err)
}
return bz
}
func (msg BondMsg) GetSigners() []sdk.Address {
return []sdk.Address{msg.Address}
}
// -------------------------
// UnbondMsg
type UnbondMsg struct {
Address sdk.Address `json:"address"`
}
func NewUnbondMsg(addr sdk.Address) UnbondMsg {
return UnbondMsg{
Address: addr,
}
}
func (msg UnbondMsg) Type() string {
return "staking"
}
func (msg UnbondMsg) ValidateBasic() sdk.Error {
return nil
}
func (msg UnbondMsg) Get(key interface{}) interface{} {
return nil
}
func (msg UnbondMsg) GetSignBytes() []byte {
bz, err := json.Marshal(msg)
if err != nil {
panic(err)
}
return bz
}
func (msg UnbondMsg) GetSigners() []sdk.Address {
return []sdk.Address{msg.Address}
}