From 75674a9ec30a7e63c63c355edcbb861bec89a4ea Mon Sep 17 00:00:00 2001 From: Adrian Brink Date: Sun, 18 Mar 2018 22:37:10 +0100 Subject: [PATCH] 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. --- .gitignore | 3 + Makefile | 2 +- examples/basecoin/app/app.go | 28 +++++--- examples/basecoin/cmd/basecli/main.go | 11 ++- server/start_test.go | 2 +- x/staking/commands/commands.go | 100 ++++++++++++++++++++++++++ x/staking/errors.go | 26 +++++++ x/staking/handler.go | 69 ++++++++++++++++++ x/staking/mapper.go | 84 ++++++++++++++++++++++ x/staking/mapper_test.go | 50 +++++++++++++ x/staking/types.go | 87 ++++++++++++++++++++++ 11 files changed, 446 insertions(+), 16 deletions(-) create mode 100644 x/staking/commands/commands.go create mode 100644 x/staking/errors.go create mode 100644 x/staking/handler.go create mode 100644 x/staking/mapper.go create mode 100644 x/staking/mapper_test.go create mode 100644 x/staking/types.go diff --git a/.gitignore b/.gitignore index a8961a7111..707ded5508 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ docs/_build coverage.txt profile.out .vscode +coverage.txt +profile.out +client/lcd/keys.db/ ### Vagrant ### .vagrant/ diff --git a/Makefile b/Makefile index 464cb37872..447d051c02 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/examples/basecoin/app/app.go b/examples/basecoin/app/app.go index 05a659409a..bfa34045f8 100644 --- a/examples/basecoin/app/app.go +++ b/examples/basecoin/app/app.go @@ -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 diff --git a/examples/basecoin/cmd/basecli/main.go b/examples/basecoin/cmd/basecli/main.go index 50447ba236..a8ee8c1cf4 100644 --- a/examples/basecoin/cmd/basecli/main.go +++ b/examples/basecoin/cmd/basecli/main.go @@ -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 diff --git a/server/start_test.go b/server/start_test.go index 0bd8b564b0..2657c52234 100644 --- a/server/start_test.go +++ b/server/start_test.go @@ -1,7 +1,7 @@ package server import ( - //"os" + // "os" "testing" "time" diff --git a/x/staking/commands/commands.go b/x/staking/commands/commands.go new file mode 100644 index 0000000000..7c9cacf69d --- /dev/null +++ b/x/staking/commands/commands.go @@ -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 +} diff --git a/x/staking/errors.go b/x/staking/errors.go new file mode 100644 index 0000000000..1e3a26a02c --- /dev/null +++ b/x/staking/errors.go @@ -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) +} diff --git a/x/staking/handler.go b/x/staking/handler.go new file mode 100644 index 0000000000..c14756b7ab --- /dev/null +++ b/x/staking/handler.go @@ -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}, + } +} diff --git a/x/staking/mapper.go b/x/staking/mapper.go new file mode 100644 index 0000000000..dbc7a1ebdc --- /dev/null +++ b/x/staking/mapper.go @@ -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 +} diff --git a/x/staking/mapper_test.go b/x/staking/mapper_test.go new file mode 100644 index 0000000000..3a84d5eca4 --- /dev/null +++ b/x/staking/mapper_test.go @@ -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) +} diff --git a/x/staking/types.go b/x/staking/types.go new file mode 100644 index 0000000000..0ea1796ae8 --- /dev/null +++ b/x/staking/types.go @@ -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} +}