Merge pull request #655 from cosmos/adrian/simple_staking

Adrian/simple staking
This commit is contained in:
Ethan Buchman 2018-03-21 00:52:02 +01:00 committed by GitHub
commit 328dd2f73f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 519 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
}

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

@ -0,0 +1,31 @@
package staking
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
// Staking errors reserve 300 - 399.
CodeEmptyValidator sdk.CodeType = 300
CodeInvalidUnbond sdk.CodeType = 301
CodeEmptyStake sdk.CodeType = 302
)
func ErrEmptyValidator() sdk.Error {
return newError(CodeEmptyValidator, "")
}
func ErrInvalidUnbond() sdk.Error {
return newError(CodeInvalidUnbond, "")
}
func ErrEmptyStake() sdk.Error {
return newError(CodeEmptyStake, "")
}
// -----------------------------
// 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},
}
}

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

@ -0,0 +1,91 @@
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 bondInfo{}
}
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.isEmpty() {
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.isEmpty() {
return crypto.PubKey{}, 0, ErrInvalidUnbond()
}
sm.deleteBondInfo(ctx, addr)
return bi.PubKey, bi.Power, nil
}
type bondInfo struct {
PubKey crypto.PubKey
Power int64
}
func (bi bondInfo) isEmpty() bool {
if bi == (bondInfo{}) {
return true
}
return false
}

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

@ -0,0 +1,76 @@
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.Equal(t, bi, bondInfo{})
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)
}
func TestBonding(t *testing.T) {
ms, capKey := setupMultiStore()
ctx := sdk.NewContext(ms, abci.Header{}, false, nil)
stakingMapper := NewMapper(capKey)
addr := sdk.Address([]byte("some-address"))
privKey := crypto.GenPrivKeyEd25519()
pubKey := privKey.PubKey()
_, _, err := stakingMapper.Unbond(ctx, addr)
assert.Equal(t, err, ErrInvalidUnbond())
_, err = stakingMapper.Bond(ctx, addr, pubKey, 10)
assert.Nil(t, err)
power, err := stakingMapper.Bond(ctx, addr, pubKey, 10)
assert.Equal(t, int64(20), power)
pk, _, err := stakingMapper.Unbond(ctx, addr)
assert.Nil(t, err)
assert.Equal(t, pubKey, pk)
_, _, err = stakingMapper.Unbond(ctx, addr)
assert.Equal(t, err, ErrInvalidUnbond())
}

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

@ -0,0 +1,91 @@
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 {
if msg.Stake.IsZero() {
return ErrEmptyStake()
}
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}
}

31
x/staking/types_test.go Normal file
View File

@ -0,0 +1,31 @@
package staking
import (
"testing"
"github.com/stretchr/testify/assert"
crypto "github.com/tendermint/go-crypto"
sdk "github.com/cosmos/cosmos-sdk/types"
)
func TestBondMsgValidation(t *testing.T) {
privKey := crypto.GenPrivKeyEd25519()
cases := []struct {
valid bool
bondMsg BondMsg
}{
{true, NewBondMsg(sdk.Address{}, sdk.Coin{"mycoin", 5}, privKey.PubKey())},
{false, NewBondMsg(sdk.Address{}, sdk.Coin{"mycoin", 0}, privKey.PubKey())},
}
for i, tc := range cases {
err := tc.bondMsg.ValidateBasic()
if tc.valid {
assert.Nil(t, err, "%d: %+v", i, err)
} else {
assert.NotNil(t, err, "%d", i)
}
}
}