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-20 12:14:35 +01:00
parent 4bfa40adbd
commit 75674a9ec3
11 changed files with 446 additions and 16 deletions
+100
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
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
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
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
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
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}
}