diff --git a/.gitignore b/.gitignore index a8961a71119c..707ded5508b5 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 464cb3787250..447d051c02d7 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 05a659409a0a..bfa34045f858 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 50447ba23648..a8ee8c1cf42a 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 0bd8b564b065..2657c5223483 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 000000000000..7c9cacf69d81 --- /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 000000000000..1e3a26a02ce1 --- /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 000000000000..c14756b7ab59 --- /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 000000000000..dbc7a1ebdcbd --- /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 000000000000..3a84d5eca4da --- /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 000000000000..0ea1796ae8e7 --- /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} +}