diff --git a/Gopkg.lock b/Gopkg.lock index 9ca4de6855..9c8b9f0ca3 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -11,13 +11,13 @@ branch = "master" name = "github.com/btcsuite/btcd" packages = ["btcec"] - revision = "1432d294a5b055c297457c25434efbf13384cc46" + revision = "86fed781132ac890ee03e906e4ecd5d6fa180c64" [[projects]] branch = "master" name = "github.com/cosmos/bech32cosmos" packages = ["go"] - revision = "efca97cd8c0852c44d96dfdcc70565c306eddff0" + revision = "c12e4b6ed52acc1d35fea49acec35f980914dc95" [[projects]] name = "github.com/davecgh/go-spew" @@ -256,7 +256,7 @@ "leveldb/table", "leveldb/util" ] - revision = "e6d6b529196422703d54ff5c40e79809ec2020b3" + revision = "5d6fca44a948d2be89a9702de7717f0168403d3d" [[projects]] name = "github.com/tendermint/abci" @@ -267,8 +267,8 @@ "server", "types" ] - revision = "78a8905690ef54f9d57e3b2b0ee7ad3a04ef3f1f" - version = "v0.10.3" + revision = "f9dce537281ffba5d1e047e6729429f7e5fb90c9" + version = "v0.11.0-rc0" [[projects]] branch = "master" @@ -298,17 +298,14 @@ revision = "915416979bf70efa4bcbf1c6cd5d64c5fff9fc19" version = "v0.6.2" -[[projects]] - name = "github.com/tendermint/go-wire" - packages = ["."] - revision = "fa721242b042ecd4c6ed1a934ee740db4f74e45c" - version = "v0.7.3" - [[projects]] name = "github.com/tendermint/iavl" - packages = ["."] - revision = "fd37a0fa3a7454423233bc3d5ea828f38e0af787" - version = "v0.7.0" + packages = [ + ".", + "sha256truncated" + ] + revision = "c9206995e8f948e99927f5084a88a7e94ca256da" + version = "v0.8.0-rc0" [[projects]] name = "github.com/tendermint/tendermint" @@ -319,6 +316,9 @@ "consensus", "consensus/types", "evidence", + "libs/events", + "libs/pubsub", + "libs/pubsub/query", "lite", "lite/client", "lite/errors", @@ -347,8 +347,8 @@ "types/priv_validator", "version" ] - revision = "018e096748bafe1d2d1e69b909e4158f3b26f6b2" - version = "v0.19.5-rc1" + revision = "73de99ecab464208f6ea3a96525f4e4b78425e61" + version = "v0.20.0-rc0" [[projects]] name = "github.com/tendermint/tmlibs" @@ -361,12 +361,10 @@ "db", "flowrate", "log", - "merkle", - "pubsub", - "pubsub/query" + "merkle" ] - revision = "cc5f287c4798ffe88c04d02df219ecb6932080fd" - version = "v0.8.3-rc0" + revision = "d970af87248a4e162590300dbb74e102183a417d" + version = "v0.8.3" [[projects]] branch = "master" @@ -382,7 +380,7 @@ "ripemd160", "salsa20/salsa" ] - revision = "1a580b3eff7814fc9b40602fd35256c63b50f491" + revision = "ab813273cd59e1333f7ae7bff5d027d4aadf528c" [[projects]] branch = "master" @@ -396,13 +394,13 @@ "internal/timeseries", "trace" ] - revision = "57065200b4b034a1c8ad54ff77069408c2218ae6" + revision = "1e491301e022f8f977054da4c2d852decd59571f" [[projects]] branch = "master" name = "golang.org/x/sys" packages = ["unix"] - revision = "7c87d13f8e835d2fb3a70a2912c811ed0c1d241b" + revision = "c11f84a56e43e20a78cee75a7c034031ecf57d1f" [[projects]] name = "golang.org/x/text" @@ -463,6 +461,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "f0c6224dc5f30c1a7dea716d619665831ea0932b0eb9afc6ac897dbc459134fa" + inputs-digest = "a6a5d886519fa9ca97a23715faa852ee14ecb5337e03641d19ea3d3d1c392fee" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index a1adc79ac7..deabcd6452 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -54,43 +54,37 @@ [[constraint]] name = "github.com/tendermint/abci" - version = "~0.10.3" + version = "0.11.0-rc0" [[constraint]] name = "github.com/tendermint/go-crypto" version = "~0.6.2" -[[override]] - name = "github.com/tendermint/go-wire" - version = "0.7.3" - [[constraint]] name = "github.com/tendermint/go-amino" - version = "~0.9.9" + version = "=0.9.9" [[constraint]] name = "github.com/tendermint/iavl" - version = "~0.7.0" + version = "0.8.0-rc0" [[constraint]] name = "github.com/tendermint/tendermint" - version = "0.19.5-rc1" + version = "0.20.0-rc0" -[[override]] +[[constraint]] name = "github.com/tendermint/tmlibs" version = "~0.8.3-rc0" + +[[constraint]] + name = "github.com/cosmos/bech32cosmos" + branch = "master" # this got updated and broke, so locked to an old working commit ... [[override]] name = "google.golang.org/genproto" revision = "7fd901a49ba6a7f87732eb344f6e3c5b19d1b200" - [[constraint]] - name = "github.com/cosmos/bech32cosmos" - branch = "master" - - [prune] go-tests = true unused-packages = true - diff --git a/baseapp/baseapp.go b/baseapp/baseapp.go index 4ce8a05d9b..cdc5ffcf7a 100644 --- a/baseapp/baseapp.go +++ b/baseapp/baseapp.go @@ -65,9 +65,10 @@ type BaseApp struct { // See methods setCheckState and setDeliverState. // .valUpdates accumulate in DeliverTx and are reset in BeginBlock. // QUESTION: should we put valUpdates in the deliverState.ctx? - checkState *state // for CheckTx - deliverState *state // for DeliverTx - valUpdates []abci.Validator // cached validator changes from DeliverTx + checkState *state // for CheckTx + deliverState *state // for DeliverTx + valUpdates []abci.Validator // cached validator changes from DeliverTx + absentValidators [][]byte // absent validators from begin block } var _ abci.Application = (*BaseApp)(nil) @@ -384,6 +385,8 @@ func (app *BaseApp) BeginBlock(req abci.RequestBeginBlock) (res abci.ResponseBeg if app.beginBlocker != nil { res = app.beginBlocker(app.deliverState.ctx, req) } + // set the absent validators for addition to context in deliverTx + app.absentValidators = req.AbsentValidators return } @@ -493,6 +496,7 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk ctx = app.checkState.ctx.WithTxBytes(txBytes) } else { ctx = app.deliverState.ctx.WithTxBytes(txBytes) + ctx = ctx.WithAbsentValidators(app.absentValidators) } // Simulate a DeliverTx for gas calculation diff --git a/baseapp/baseapp_test.go b/baseapp/baseapp_test.go index 61498b1b19..0b825e1ee8 100644 --- a/baseapp/baseapp_test.go +++ b/baseapp/baseapp_test.go @@ -183,7 +183,7 @@ func TestInitChainer(t *testing.T) { // set initChainer and try again - should see the value app.SetInitChainer(initChainer) - app.InitChain(abci.RequestInitChain{AppStateBytes: []byte("{}")}) // must have valid JSON genesis file, even if empty + app.InitChain(abci.RequestInitChain{GenesisBytes: []byte("{}")}) // must have valid JSON genesis file, even if empty app.Commit() res = app.Query(query) assert.Equal(t, value, res.Value) diff --git a/cmd/gaia/app/app.go b/cmd/gaia/app/app.go index dbecada004..4fdb6a6c92 100644 --- a/cmd/gaia/app/app.go +++ b/cmd/gaia/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/slashing" "github.com/cosmos/cosmos-sdk/x/stake" ) @@ -34,10 +35,11 @@ type GaiaApp struct { cdc *wire.Codec // keys to access the substores - keyMain *sdk.KVStoreKey - keyAccount *sdk.KVStoreKey - keyIBC *sdk.KVStoreKey - keyStake *sdk.KVStoreKey + keyMain *sdk.KVStoreKey + keyAccount *sdk.KVStoreKey + keyIBC *sdk.KVStoreKey + keyStake *sdk.KVStoreKey + keySlashing *sdk.KVStoreKey // Manage getting and setting accounts accountMapper auth.AccountMapper @@ -45,6 +47,7 @@ type GaiaApp struct { coinKeeper bank.Keeper ibcMapper ibc.Mapper stakeKeeper stake.Keeper + slashingKeeper slashing.Keeper } func NewGaiaApp(logger log.Logger, db dbm.DB) *GaiaApp { @@ -52,12 +55,13 @@ func NewGaiaApp(logger log.Logger, db dbm.DB) *GaiaApp { // create your application object var app = &GaiaApp{ - BaseApp: bam.NewBaseApp(appName, cdc, logger, db), - cdc: cdc, - keyMain: sdk.NewKVStoreKey("main"), - keyAccount: sdk.NewKVStoreKey("acc"), - keyIBC: sdk.NewKVStoreKey("ibc"), - keyStake: sdk.NewKVStoreKey("stake"), + BaseApp: bam.NewBaseApp(appName, cdc, logger, db), + cdc: cdc, + keyMain: sdk.NewKVStoreKey("main"), + keyAccount: sdk.NewKVStoreKey("acc"), + keyIBC: sdk.NewKVStoreKey("ibc"), + keyStake: sdk.NewKVStoreKey("stake"), + keySlashing: sdk.NewKVStoreKey("slashing"), } // define the accountMapper @@ -71,6 +75,7 @@ func NewGaiaApp(logger log.Logger, db dbm.DB) *GaiaApp { app.coinKeeper = bank.NewKeeper(app.accountMapper) app.ibcMapper = ibc.NewMapper(app.cdc, app.keyIBC, app.RegisterCodespace(ibc.DefaultCodespace)) app.stakeKeeper = stake.NewKeeper(app.cdc, app.keyStake, app.coinKeeper, app.RegisterCodespace(stake.DefaultCodespace)) + app.slashingKeeper = slashing.NewKeeper(app.cdc, app.keySlashing, app.stakeKeeper, app.RegisterCodespace(slashing.DefaultCodespace)) // register message routes app.Router(). @@ -80,9 +85,10 @@ func NewGaiaApp(logger log.Logger, db dbm.DB) *GaiaApp { // initialize BaseApp app.SetInitChainer(app.initChainer) + app.SetBeginBlocker(slashing.NewBeginBlocker(app.slashingKeeper)) app.SetEndBlocker(stake.NewEndBlocker(app.stakeKeeper)) - app.MountStoresIAVL(app.keyMain, app.keyAccount, app.keyIBC, app.keyStake) app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper, app.feeCollectionKeeper)) + app.MountStoresIAVL(app.keyMain, app.keyAccount, app.keyIBC, app.keyStake, app.keySlashing) err := app.LoadLatestVersion(app.keyMain) if err != nil { cmn.Exit(err.Error()) @@ -97,6 +103,7 @@ func MakeCodec() *wire.Codec { ibc.RegisterWire(cdc) bank.RegisterWire(cdc) stake.RegisterWire(cdc) + slashing.RegisterWire(cdc) auth.RegisterWire(cdc) sdk.RegisterWire(cdc) wire.RegisterCrypto(cdc) @@ -105,7 +112,8 @@ func MakeCodec() *wire.Codec { // custom logic for gaia initialization func (app *GaiaApp) initChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { - stateJSON := req.AppStateBytes + stateJSON := req.GenesisBytes + // TODO is this now the whole genesis file? var genesisState GenesisState err := app.cdc.UnmarshalJSON(stateJSON, &genesisState) diff --git a/examples/basecoin/app/app.go b/examples/basecoin/app/app.go index 086fa32b36..a0b1f86ad6 100644 --- a/examples/basecoin/app/app.go +++ b/examples/basecoin/app/app.go @@ -104,7 +104,7 @@ func MakeCodec() *wire.Codec { // Custom logic for basecoin initialization func (app *BasecoinApp) initChainer(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { - stateJSON := req.AppStateBytes + stateJSON := req.GenesisBytes genesisState := new(types.GenesisState) err := app.cdc.UnmarshalJSON(stateJSON, genesisState) diff --git a/examples/democoin/app/app.go b/examples/democoin/app/app.go index 2075a64da0..b8b8642cf3 100644 --- a/examples/democoin/app/app.go +++ b/examples/democoin/app/app.go @@ -118,7 +118,7 @@ func MakeCodec() *wire.Codec { // custom logic for democoin initialization func (app *DemocoinApp) initChainerFn(coolKeeper cool.Keeper, powKeeper pow.Keeper) sdk.InitChainer { return func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { - stateJSON := req.AppStateBytes + stateJSON := req.GenesisBytes genesisState := new(types.GenesisState) err := app.cdc.UnmarshalJSON(stateJSON, genesisState) diff --git a/examples/democoin/x/simplestake/keeper_test.go b/examples/democoin/x/simplestake/keeper_test.go index 515c19cc59..15bd14c79b 100644 --- a/examples/democoin/x/simplestake/keeper_test.go +++ b/examples/democoin/x/simplestake/keeper_test.go @@ -35,9 +35,9 @@ func TestKeeperGetSet(t *testing.T) { cdc := wire.NewCodec() auth.RegisterBaseAccount(cdc) - ctx := sdk.NewContext(ms, abci.Header{}, false, nil, log.NewNopLogger()) accountMapper := auth.NewAccountMapper(cdc, authKey, &auth.BaseAccount{}) stakeKeeper := NewKeeper(capKey, bank.NewKeeper(accountMapper), DefaultCodespace) + ctx := sdk.NewContext(ms, abci.Header{}, false, nil, log.NewNopLogger()) addr := sdk.Address([]byte("some-address")) bi := stakeKeeper.getBondInfo(ctx, addr) diff --git a/mock/app.go b/mock/app.go index ab1a8447a5..09d7a658c7 100644 --- a/mock/app.go +++ b/mock/app.go @@ -88,7 +88,7 @@ type GenesisJSON struct { // with key/value pairs func InitChainer(key sdk.StoreKey) func(sdk.Context, abci.RequestInitChain) abci.ResponseInitChain { return func(ctx sdk.Context, req abci.RequestInitChain) abci.ResponseInitChain { - stateJSON := req.AppStateBytes + stateJSON := req.GenesisBytes genesisState := new(GenesisJSON) err := json.Unmarshal(stateJSON, genesisState) diff --git a/mock/app_test.go b/mock/app_test.go index be1d778295..50b0761c17 100644 --- a/mock/app_test.go +++ b/mock/app_test.go @@ -26,7 +26,7 @@ func TestInitApp(t *testing.T) { //TODO test validators in the init chain? req := abci.RequestInitChain{ - AppStateBytes: appState, + GenesisBytes: appState, } app.InitChain(req) app.Commit() diff --git a/types/context.go b/types/context.go index 4ab0a5d093..10ae99724f 100644 --- a/types/context.go +++ b/types/context.go @@ -31,6 +31,7 @@ type Context struct { // create a new context func NewContext(ms MultiStore, header abci.Header, isCheckTx bool, txBytes []byte, logger log.Logger) Context { + c := Context{ Context: context.Background(), pst: newThePast(), @@ -43,6 +44,7 @@ func NewContext(ms MultiStore, header abci.Header, isCheckTx bool, txBytes []byt c = c.WithIsCheckTx(isCheckTx) c = c.WithTxBytes(txBytes) c = c.WithLogger(logger) + c = c.WithAbsentValidators(nil) c = c.WithGasMeter(NewInfiniteGasMeter()) return c } @@ -128,6 +130,7 @@ const ( contextKeyIsCheckTx contextKeyTxBytes contextKeyLogger + contextKeyAbsentValidators contextKeyGasMeter ) @@ -157,6 +160,9 @@ func (c Context) TxBytes() []byte { func (c Context) Logger() log.Logger { return c.Value(contextKeyLogger).(log.Logger) } +func (c Context) AbsentValidators() [][]byte { + return c.Value(contextKeyAbsentValidators).([][]byte) +} func (c Context) GasMeter() GasMeter { return c.Value(contextKeyGasMeter).(GasMeter) } @@ -182,6 +188,9 @@ func (c Context) WithTxBytes(txBytes []byte) Context { func (c Context) WithLogger(logger log.Logger) Context { return c.withValue(contextKeyLogger, logger) } +func (c Context) WithAbsentValidators(AbsentValidators [][]byte) Context { + return c.withValue(contextKeyAbsentValidators, AbsentValidators) +} func (c Context) WithGasMeter(meter GasMeter) Context { return c.withValue(contextKeyGasMeter, meter) } diff --git a/types/stake.go b/types/stake.go index 6a1a3a95f6..bfcef7fa0c 100644 --- a/types/stake.go +++ b/types/stake.go @@ -56,8 +56,11 @@ type ValidatorSet interface { IterateValidatorsBonded(Context, func(index int64, validator Validator) (stop bool)) - Validator(Context, Address) Validator // get a particular validator by owner address - TotalPower(Context) Rat // total power of the validator set + Validator(Context, Address) Validator // get a particular validator by owner address + TotalPower(Context) Rat // total power of the validator set + Slash(Context, crypto.PubKey, int64, Rat) // slash the validator and delegators of the validator, specifying offence height & slash fraction + Revoke(Context, crypto.PubKey) // revoke a validator + Unrevoke(Context, crypto.PubKey) // unrevoke a validator } //_______________________________________________________________________________ diff --git a/types/tags.go b/types/tags.go index 95a826fd78..5a8eb1f473 100644 --- a/types/tags.go +++ b/types/tags.go @@ -25,6 +25,11 @@ func (t Tags) AppendTags(a Tags) Tags { return append(t, a...) } +// Turn tags into KVPair list +func (t Tags) ToKVPairs() []cmn.KVPair { + return []cmn.KVPair(t) +} + // New variadic tags, must be k string, v []byte repeating func NewTags(tags ...interface{}) Tags { var ret Tags diff --git a/x/slashing/errors.go b/x/slashing/errors.go new file mode 100644 index 0000000000..087dc03141 --- /dev/null +++ b/x/slashing/errors.go @@ -0,0 +1,52 @@ +//nolint +package slashing + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Local code type +type CodeType = sdk.CodeType + +const ( + // Default slashing codespace + DefaultCodespace sdk.CodespaceType = 10 + + // Invalid validator + CodeInvalidValidator CodeType = 201 + // Validator jailed + CodeValidatorJailed CodeType = 202 +) + +func ErrNoValidatorForAddress(codespace sdk.CodespaceType) sdk.Error { + return newError(codespace, CodeInvalidValidator, "That address is not associated with any known validator") +} +func ErrBadValidatorAddr(codespace sdk.CodespaceType) sdk.Error { + return newError(codespace, CodeInvalidValidator, "Validator does not exist for that address") +} +func ErrValidatorJailed(codespace sdk.CodespaceType) sdk.Error { + return newError(codespace, CodeValidatorJailed, "Validator jailed, cannot yet be unrevoked") +} + +func codeToDefaultMsg(code CodeType) string { + switch code { + case CodeInvalidValidator: + return "Invalid Validator" + case CodeValidatorJailed: + return "Validator Jailed" + default: + return sdk.CodeToDefaultMsg(code) + } +} + +func msgOrDefaultMsg(msg string, code CodeType) string { + if msg != "" { + return msg + } + return codeToDefaultMsg(code) +} + +func newError(codespace sdk.CodespaceType, code CodeType, msg string) sdk.Error { + msg = msgOrDefaultMsg(msg, code) + return sdk.NewError(codespace, code, msg) +} diff --git a/x/slashing/handler.go b/x/slashing/handler.go new file mode 100644 index 0000000000..98e9d30ada --- /dev/null +++ b/x/slashing/handler.go @@ -0,0 +1,58 @@ +package slashing + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func NewHandler(k Keeper) sdk.Handler { + return func(ctx sdk.Context, msg sdk.Msg) sdk.Result { + // NOTE msg already has validate basic run + switch msg := msg.(type) { + case MsgUnrevoke: + return handleMsgUnrevoke(ctx, msg, k) + default: + return sdk.ErrTxDecode("invalid message parse in staking module").Result() + } + } +} + +// Validators must submit a transaction to unrevoke itself after +// having been revoked (and thus unbonded) for downtime +func handleMsgUnrevoke(ctx sdk.Context, msg MsgUnrevoke, k Keeper) sdk.Result { + + // Validator must exist + validator := k.stakeKeeper.Validator(ctx, msg.ValidatorAddr) + if validator == nil { + return ErrNoValidatorForAddress(k.codespace).Result() + } + + addr := validator.GetPubKey().Address() + + // Signing info must exist + info, found := k.getValidatorSigningInfo(ctx, addr) + if !found { + return ErrNoValidatorForAddress(k.codespace).Result() + } + + // Cannot be unrevoked until out of jail + if ctx.BlockHeader().Time < info.JailedUntil { + return ErrValidatorJailed(k.codespace).Result() + } + + if ctx.IsCheckTx() { + return sdk.Result{} + } + + // Update the starting height (so the validator can't be immediately revoked again) + info.StartHeight = ctx.BlockHeight() + k.setValidatorSigningInfo(ctx, addr, info) + + // Unrevoke the validator + k.stakeKeeper.Unrevoke(ctx, validator.GetPubKey()) + + tags := sdk.NewTags("action", []byte("unrevoke"), "validator", msg.ValidatorAddr.Bytes()) + + return sdk.Result{ + Tags: tags, + } +} diff --git a/x/slashing/keeper.go b/x/slashing/keeper.go new file mode 100644 index 0000000000..a37c5a07b8 --- /dev/null +++ b/x/slashing/keeper.go @@ -0,0 +1,91 @@ +package slashing + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/stake" + crypto "github.com/tendermint/go-crypto" +) + +// Keeper of the slashing store +type Keeper struct { + storeKey sdk.StoreKey + cdc *wire.Codec + stakeKeeper stake.Keeper + + // codespace + codespace sdk.CodespaceType +} + +// NewKeeper creates a slashing keeper +func NewKeeper(cdc *wire.Codec, key sdk.StoreKey, sk stake.Keeper, codespace sdk.CodespaceType) Keeper { + keeper := Keeper{ + storeKey: key, + cdc: cdc, + stakeKeeper: sk, + codespace: codespace, + } + return keeper +} + +// handle a validator signing two blocks at the same height +func (k Keeper) handleDoubleSign(ctx sdk.Context, height int64, timestamp int64, pubkey crypto.PubKey) { + logger := ctx.Logger().With("module", "x/slashing") + age := ctx.BlockHeader().Time - timestamp + + // Double sign too old + if age > MaxEvidenceAge { + logger.Info(fmt.Sprintf("Ignored double sign from %s at height %d, age of %d past max age of %d", pubkey.Address(), height, age, MaxEvidenceAge)) + return + } + + // Double sign confirmed + logger.Info(fmt.Sprintf("Confirmed double sign from %s at height %d, age of %d less than max age of %d", pubkey.Address(), height, age, MaxEvidenceAge)) + k.stakeKeeper.Slash(ctx, pubkey, height, SlashFractionDoubleSign) +} + +// handle a validator signature, must be called once per validator per block +func (k Keeper) handleValidatorSignature(ctx sdk.Context, pubkey crypto.PubKey, signed bool) { + logger := ctx.Logger().With("module", "x/slashing") + height := ctx.BlockHeight() + if !signed { + logger.Info(fmt.Sprintf("Absent validator %s at height %d", pubkey.Address(), height)) + } + address := pubkey.Address() + + // Local index, so counts blocks validator *should* have signed + // Will use the 0-value default signing info if not present + signInfo, _ := k.getValidatorSigningInfo(ctx, address) + index := signInfo.IndexOffset % SignedBlocksWindow + signInfo.IndexOffset++ + + // Update signed block bit array & counter + // This counter just tracks the sum of the bit array + // That way we avoid needing to read/write the whole array each time + previous := k.getValidatorSigningBitArray(ctx, address, index) + if previous == signed { + // Array value at this index has not changed, no need to update counter + } else if previous && !signed { + // Array value has changed from signed to unsigned, decrement counter + k.setValidatorSigningBitArray(ctx, address, index, false) + signInfo.SignedBlocksCounter-- + } else if !previous && signed { + // Array value has changed from unsigned to signed, increment counter + k.setValidatorSigningBitArray(ctx, address, index, true) + signInfo.SignedBlocksCounter++ + } + + minHeight := signInfo.StartHeight + SignedBlocksWindow + if height > minHeight && signInfo.SignedBlocksCounter < MinSignedPerWindow { + // Downtime confirmed, slash, revoke, and jail the validator + logger.Info(fmt.Sprintf("Validator %s past min height of %d and below signed blocks threshold of %d", pubkey.Address(), minHeight, MinSignedPerWindow)) + k.stakeKeeper.Slash(ctx, pubkey, height, SlashFractionDowntime) + k.stakeKeeper.Revoke(ctx, pubkey) + signInfo.JailedUntil = ctx.BlockHeader().Time + DowntimeUnbondDuration + } + + // Set the updated signing info + k.setValidatorSigningInfo(ctx, address, signInfo) +} diff --git a/x/slashing/keeper_test.go b/x/slashing/keeper_test.go new file mode 100644 index 0000000000..3d6da8c232 --- /dev/null +++ b/x/slashing/keeper_test.go @@ -0,0 +1,131 @@ +package slashing + +import ( + "testing" + + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/abci/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/stake" +) + +func TestHandleDoubleSign(t *testing.T) { + + // initial setup + ctx, ck, sk, keeper := createTestInput(t) + addr, val, amt := addrs[0], pks[0], int64(100) + got := stake.NewHandler(sk)(ctx, newTestMsgDeclareCandidacy(addr, val, amt)) + require.True(t, got.IsOK()) + sk.Tick(ctx) + require.Equal(t, ck.GetCoins(ctx, addr), sdk.Coins{{sk.GetParams(ctx).BondDenom, initCoins - amt}}) + require.Equal(t, sdk.NewRat(amt), sk.Validator(ctx, addr).GetPower()) + + // double sign less than max age + keeper.handleDoubleSign(ctx, 0, 0, val) + require.Equal(t, sdk.NewRat(amt).Mul(sdk.NewRat(19).Quo(sdk.NewRat(20))), sk.Validator(ctx, addr).GetPower()) + ctx = ctx.WithBlockHeader(abci.Header{Time: 300}) + + // double sign past max age + keeper.handleDoubleSign(ctx, 0, 0, val) + require.Equal(t, sdk.NewRat(amt).Mul(sdk.NewRat(19).Quo(sdk.NewRat(20))), sk.Validator(ctx, addr).GetPower()) +} + +func TestHandleAbsentValidator(t *testing.T) { + + // initial setup + ctx, ck, sk, keeper := createTestInput(t) + addr, val, amt := addrs[0], pks[0], int64(100) + sh := stake.NewHandler(sk) + slh := NewHandler(keeper) + got := sh(ctx, newTestMsgDeclareCandidacy(addr, val, amt)) + require.True(t, got.IsOK()) + sk.Tick(ctx) + require.Equal(t, ck.GetCoins(ctx, addr), sdk.Coins{{sk.GetParams(ctx).BondDenom, initCoins - amt}}) + require.Equal(t, sdk.NewRat(amt), sk.Validator(ctx, addr).GetPower()) + info, found := keeper.getValidatorSigningInfo(ctx, val.Address()) + require.False(t, found) + require.Equal(t, int64(0), info.StartHeight) + require.Equal(t, int64(0), info.IndexOffset) + require.Equal(t, int64(0), info.SignedBlocksCounter) + require.Equal(t, int64(0), info.JailedUntil) + height := int64(0) + + // 1000 first blocks OK + for ; height < 1000; height++ { + ctx = ctx.WithBlockHeight(height) + keeper.handleValidatorSignature(ctx, val, true) + } + info, found = keeper.getValidatorSigningInfo(ctx, val.Address()) + require.True(t, found) + require.Equal(t, int64(0), info.StartHeight) + require.Equal(t, SignedBlocksWindow, info.SignedBlocksCounter) + + // 50 blocks missed + for ; height < 1050; height++ { + ctx = ctx.WithBlockHeight(height) + keeper.handleValidatorSignature(ctx, val, false) + } + info, found = keeper.getValidatorSigningInfo(ctx, val.Address()) + require.True(t, found) + require.Equal(t, int64(0), info.StartHeight) + require.Equal(t, SignedBlocksWindow-50, info.SignedBlocksCounter) + + // validator should be bonded still + validator, _ := sk.GetValidatorByPubKey(ctx, val) + require.Equal(t, sdk.Bonded, validator.GetStatus()) + pool := sk.GetPool(ctx) + require.Equal(t, int64(100), pool.BondedTokens) + + // 51st block missed + ctx = ctx.WithBlockHeight(height) + keeper.handleValidatorSignature(ctx, val, false) + info, found = keeper.getValidatorSigningInfo(ctx, val.Address()) + require.True(t, found) + require.Equal(t, int64(0), info.StartHeight) + require.Equal(t, SignedBlocksWindow-51, info.SignedBlocksCounter) + + // validator should have been revoked + validator, _ = sk.GetValidatorByPubKey(ctx, val) + require.Equal(t, sdk.Unbonded, validator.GetStatus()) + + // unrevocation should fail prior to jail expiration + got = slh(ctx, NewMsgUnrevoke(addr)) + require.False(t, got.IsOK()) + + // unrevocation should succeed after jail expiration + ctx = ctx.WithBlockHeader(abci.Header{Time: int64(86400 * 2)}) + got = slh(ctx, NewMsgUnrevoke(addr)) + require.True(t, got.IsOK()) + + // validator should be rebonded now + validator, _ = sk.GetValidatorByPubKey(ctx, val) + require.Equal(t, sdk.Bonded, validator.GetStatus()) + + // validator should have been slashed + pool = sk.GetPool(ctx) + require.Equal(t, int64(99), pool.BondedTokens) + + // validator start height should have been changed + info, found = keeper.getValidatorSigningInfo(ctx, val.Address()) + require.True(t, found) + require.Equal(t, height, info.StartHeight) + require.Equal(t, SignedBlocksWindow-51, info.SignedBlocksCounter) + + // validator should not be immediately revoked again + height++ + ctx = ctx.WithBlockHeight(height) + keeper.handleValidatorSignature(ctx, val, false) + validator, _ = sk.GetValidatorByPubKey(ctx, val) + require.Equal(t, sdk.Bonded, validator.GetStatus()) + + // validator should be revoked again after 100 unsigned blocks + nextHeight := height + 100 + for ; height <= nextHeight; height++ { + ctx = ctx.WithBlockHeight(height) + keeper.handleValidatorSignature(ctx, val, false) + } + validator, _ = sk.GetValidatorByPubKey(ctx, val) + require.Equal(t, sdk.Unbonded, validator.GetStatus()) +} diff --git a/x/slashing/msg.go b/x/slashing/msg.go new file mode 100644 index 0000000000..d2676af81a --- /dev/null +++ b/x/slashing/msg.go @@ -0,0 +1,46 @@ +package slashing + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" +) + +var cdc = wire.NewCodec() + +// name to identify transaction types +const MsgType = "slashing" + +// verify interface at compile time +var _ sdk.Msg = &MsgUnrevoke{} + +// MsgUnrevoke - struct for unrevoking revoked validator +type MsgUnrevoke struct { + ValidatorAddr sdk.Address `json:"address"` // address of the validator owner +} + +func NewMsgUnrevoke(validatorAddr sdk.Address) MsgUnrevoke { + return MsgUnrevoke{ + ValidatorAddr: validatorAddr, + } +} + +//nolint +func (msg MsgUnrevoke) Type() string { return MsgType } +func (msg MsgUnrevoke) GetSigners() []sdk.Address { return []sdk.Address{msg.ValidatorAddr} } + +// get the bytes for the message signer to sign on +func (msg MsgUnrevoke) GetSignBytes() []byte { + b, err := cdc.MarshalJSON(msg) + if err != nil { + panic(err) + } + return b +} + +// quick validity check +func (msg MsgUnrevoke) ValidateBasic() sdk.Error { + if msg.ValidatorAddr == nil { + return ErrBadValidatorAddr(DefaultCodespace) + } + return nil +} diff --git a/x/slashing/params.go b/x/slashing/params.go new file mode 100644 index 0000000000..3bba85fa66 --- /dev/null +++ b/x/slashing/params.go @@ -0,0 +1,37 @@ +package slashing + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + // MaxEvidenceAge - Max age for evidence - 21 days (3 weeks) + // TODO Should this be a governance parameter or just modifiable with SoftwareUpgradeProposals? + // MaxEvidenceAge = 60 * 60 * 24 * 7 * 3 + // TODO Temporarily set to 2 minutes for testnets. + MaxEvidenceAge int64 = 60 * 2 + + // SignedBlocksWindow - sliding window for downtime slashing + // TODO Governance parameter? + // TODO Temporarily set to 100 blocks for testnets + SignedBlocksWindow int64 = 100 + + // Downtime slashing threshold - 50% + // TODO Governance parameter? + MinSignedPerWindow int64 = SignedBlocksWindow / 2 + + // Downtime unbond duration + // TODO Governance parameter? + // TODO Temporarily set to 10 minutes for testnets + DowntimeUnbondDuration int64 = 60 * 10 +) + +var ( + // SlashFractionDoubleSign - currently 5% + // TODO Governance parameter? + SlashFractionDoubleSign = sdk.NewRat(1).Quo(sdk.NewRat(20)) + + // SlashFractionDowntime - currently 1% + // TODO Governance parameter? + SlashFractionDowntime = sdk.NewRat(1).Quo(sdk.NewRat(100)) +) diff --git a/x/slashing/signing_info.go b/x/slashing/signing_info.go new file mode 100644 index 0000000000..05285b6809 --- /dev/null +++ b/x/slashing/signing_info.go @@ -0,0 +1,66 @@ +package slashing + +import ( + "encoding/binary" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// Stored by *validator* address (not owner address) +func (k Keeper) getValidatorSigningInfo(ctx sdk.Context, address sdk.Address) (info validatorSigningInfo, found bool) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(validatorSigningInfoKey(address)) + if bz == nil { + found = false + return + } + k.cdc.MustUnmarshalBinary(bz, &info) + found = true + return +} + +// Stored by *validator* address (not owner address) +func (k Keeper) setValidatorSigningInfo(ctx sdk.Context, address sdk.Address, info validatorSigningInfo) { + store := ctx.KVStore(k.storeKey) + bz := k.cdc.MustMarshalBinary(info) + store.Set(validatorSigningInfoKey(address), bz) +} + +// Stored by *validator* address (not owner address) +func (k Keeper) getValidatorSigningBitArray(ctx sdk.Context, address sdk.Address, index int64) (signed bool) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(validatorSigningBitArrayKey(address, index)) + if bz == nil { + // lazy: treat empty key as unsigned + signed = false + return + } + k.cdc.MustUnmarshalBinary(bz, &signed) + return +} + +// Stored by *validator* address (not owner address) +func (k Keeper) setValidatorSigningBitArray(ctx sdk.Context, address sdk.Address, index int64, signed bool) { + store := ctx.KVStore(k.storeKey) + bz := k.cdc.MustMarshalBinary(signed) + store.Set(validatorSigningBitArrayKey(address, index), bz) +} + +type validatorSigningInfo struct { + StartHeight int64 `json:"start_height"` // height at which validator was first a candidate OR was unrevoked + IndexOffset int64 `json:"index_offset"` // index offset into signed block bit array + JailedUntil int64 `json:"jailed_until"` // timestamp validator cannot be unrevoked until + SignedBlocksCounter int64 `json:"signed_blocks_counter"` // signed blocks counter (to avoid scanning the array every time) +} + +// Stored by *validator* address (not owner address) +func validatorSigningInfoKey(v sdk.Address) []byte { + return append([]byte{0x01}, v.Bytes()...) +} + +// Stored by *validator* address (not owner address) +func validatorSigningBitArrayKey(v sdk.Address, i int64) []byte { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, uint64(i)) + return append([]byte{0x02}, append(v.Bytes(), b...)...) +} diff --git a/x/slashing/signing_info_test.go b/x/slashing/signing_info_test.go new file mode 100644 index 0000000000..8000d67450 --- /dev/null +++ b/x/slashing/signing_info_test.go @@ -0,0 +1,35 @@ +package slashing + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetSetValidatorSigningInfo(t *testing.T) { + ctx, _, _, keeper := createTestInput(t) + info, found := keeper.getValidatorSigningInfo(ctx, addrs[0]) + require.False(t, found) + newInfo := validatorSigningInfo{ + StartHeight: int64(4), + IndexOffset: int64(3), + JailedUntil: int64(2), + SignedBlocksCounter: int64(10), + } + keeper.setValidatorSigningInfo(ctx, addrs[0], newInfo) + info, found = keeper.getValidatorSigningInfo(ctx, addrs[0]) + require.True(t, found) + require.Equal(t, info.StartHeight, int64(4)) + require.Equal(t, info.IndexOffset, int64(3)) + require.Equal(t, info.JailedUntil, int64(2)) + require.Equal(t, info.SignedBlocksCounter, int64(10)) +} + +func TestGetSetValidatorSigningBitArray(t *testing.T) { + ctx, _, _, keeper := createTestInput(t) + signed := keeper.getValidatorSigningBitArray(ctx, addrs[0], 0) + require.False(t, signed) // treat empty key as unsigned + keeper.setValidatorSigningBitArray(ctx, addrs[0], 0, true) + signed = keeper.getValidatorSigningBitArray(ctx, addrs[0], 0) + require.True(t, signed) // now should be signed +} diff --git a/x/slashing/test_common.go b/x/slashing/test_common.go new file mode 100644 index 0000000000..0228e9498b --- /dev/null +++ b/x/slashing/test_common.go @@ -0,0 +1,95 @@ +package slashing + +import ( + "encoding/hex" + "os" + "testing" + + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/abci/types" + crypto "github.com/tendermint/go-crypto" + dbm "github.com/tendermint/tmlibs/db" + "github.com/tendermint/tmlibs/log" + + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/wire" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/cosmos/cosmos-sdk/x/bank" + "github.com/cosmos/cosmos-sdk/x/stake" +) + +var ( + addrs = []sdk.Address{ + testAddr("A58856F0FD53BF058B4909A21AEC019107BA6160"), + testAddr("A58856F0FD53BF058B4909A21AEC019107BA6161"), + } + pks = []crypto.PubKey{ + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB50"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB51"), + } + initCoins int64 = 200 +) + +func createTestCodec() *wire.Codec { + cdc := wire.NewCodec() + sdk.RegisterWire(cdc) + auth.RegisterWire(cdc) + bank.RegisterWire(cdc) + stake.RegisterWire(cdc) + wire.RegisterCrypto(cdc) + return cdc +} + +func createTestInput(t *testing.T) (sdk.Context, bank.Keeper, stake.Keeper, Keeper) { + keyAcc := sdk.NewKVStoreKey("acc") + keyStake := sdk.NewKVStoreKey("stake") + keySlashing := sdk.NewKVStoreKey("slashing") + db := dbm.NewMemDB() + ms := store.NewCommitMultiStore(db) + ms.MountStoreWithDB(keyAcc, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keyStake, sdk.StoreTypeIAVL, db) + ms.MountStoreWithDB(keySlashing, sdk.StoreTypeIAVL, db) + err := ms.LoadLatestVersion() + require.Nil(t, err) + ctx := sdk.NewContext(ms, abci.Header{}, false, nil, log.NewTMLogger(os.Stdout)) + cdc := createTestCodec() + accountMapper := auth.NewAccountMapper(cdc, keyAcc, &auth.BaseAccount{}) + ck := bank.NewKeeper(accountMapper) + sk := stake.NewKeeper(cdc, keyStake, ck, stake.DefaultCodespace) + genesis := stake.DefaultGenesisState() + genesis.Pool.LooseUnbondedTokens = initCoins * int64(len(addrs)) + stake.InitGenesis(ctx, sk, genesis) + for _, addr := range addrs { + ck.AddCoins(ctx, addr, sdk.Coins{ + {sk.GetParams(ctx).BondDenom, initCoins}, + }) + } + keeper := NewKeeper(cdc, keySlashing, sk, DefaultCodespace) + return ctx, ck, sk, keeper +} + +func newPubKey(pk string) (res crypto.PubKey) { + pkBytes, err := hex.DecodeString(pk) + if err != nil { + panic(err) + } + var pkEd crypto.PubKeyEd25519 + copy(pkEd[:], pkBytes[:]) + return pkEd +} + +func testAddr(addr string) sdk.Address { + res := []byte(addr) + return res +} + +func newTestMsgDeclareCandidacy(address sdk.Address, pubKey crypto.PubKey, amt int64) stake.MsgDeclareCandidacy { + return stake.MsgDeclareCandidacy{ + Description: stake.Description{}, + ValidatorAddr: address, + PubKey: pubKey, + Bond: sdk.Coin{"steak", amt}, + } +} diff --git a/x/slashing/tick.go b/x/slashing/tick.go new file mode 100644 index 0000000000..402c8997b8 --- /dev/null +++ b/x/slashing/tick.go @@ -0,0 +1,58 @@ +package slashing + +import ( + "encoding/binary" + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/abci/types" + crypto "github.com/tendermint/go-crypto" + tmtypes "github.com/tendermint/tendermint/types" +) + +func NewBeginBlocker(sk Keeper) sdk.BeginBlocker { + return func(ctx sdk.Context, req abci.RequestBeginBlock) abci.ResponseBeginBlock { + // Tag the height + heightBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(heightBytes, uint64(req.Header.Height)) + tags := sdk.NewTags("height", heightBytes) + + // Deal with any equivocation evidence + for _, evidence := range req.ByzantineValidators { + var pk crypto.PubKey + sk.cdc.MustUnmarshalBinary(evidence.PubKey, &pk) + switch string(evidence.Type) { + case tmtypes.DUPLICATE_VOTE: + sk.handleDoubleSign(ctx, evidence.Height, evidence.Time, pk) + default: + ctx.Logger().With("module", "x/slashing").Error(fmt.Sprintf("Ignored unknown evidence type: %s", string(evidence.Type))) + } + } + + // Figure out which validators were absent + absent := make(map[crypto.PubKey]struct{}) + for _, pubkey := range req.AbsentValidators { + var pk crypto.PubKey + sk.cdc.MustUnmarshalBinary(pubkey, &pk) + absent[pk] = struct{}{} + } + + // Iterate over all the validators which *should* have signed this block + sk.stakeKeeper.IterateValidatorsBonded(ctx, func(_ int64, validator sdk.Validator) (stop bool) { + pubkey := validator.GetPubKey() + present := true + if _, ok := absent[pubkey]; ok { + present = false + } + sk.handleValidatorSignature(ctx, pubkey, present) + return false + }) + + // Return the begin block response + // TODO Return something composable, so other modules can also have BeginBlockers + // TODO Add some more tags so clients can track slashing events + return abci.ResponseBeginBlock{ + Tags: tags.ToKVPairs(), + } + } +} diff --git a/x/slashing/wire.go b/x/slashing/wire.go new file mode 100644 index 0000000000..465a06587e --- /dev/null +++ b/x/slashing/wire.go @@ -0,0 +1,12 @@ +package slashing + +import ( + "github.com/cosmos/cosmos-sdk/wire" +) + +// Register concrete types on wire codec +func RegisterWire(cdc *wire.Codec) { + cdc.RegisterConcrete(MsgUnrevoke{}, "cosmos-sdk/MsgUnrevoke", nil) +} + +var cdcEmpty = wire.NewCodec() diff --git a/x/stake/errors.go b/x/stake/errors.go index 83c38d528d..77090d9dc3 100644 --- a/x/stake/errors.go +++ b/x/stake/errors.go @@ -16,6 +16,7 @@ const ( CodeInvalidValidator CodeType = 201 CodeInvalidBond CodeType = 202 CodeInvalidInput CodeType = 203 + CodeValidatorJailed CodeType = 204 CodeUnauthorized CodeType = sdk.CodeUnauthorized CodeInternal CodeType = sdk.CodeInternal CodeUnknownRequest CodeType = sdk.CodeUnknownRequest diff --git a/x/stake/genesis.go b/x/stake/genesis.go index be8d0dbe44..0a7c528bf6 100644 --- a/x/stake/genesis.go +++ b/x/stake/genesis.go @@ -29,14 +29,25 @@ func DefaultGenesisState() GenesisState { // InitGenesis - store genesis parameters func InitGenesis(ctx sdk.Context, k Keeper, data GenesisState) { + store := ctx.KVStore(k.storeKey) k.setPool(ctx, data.Pool) k.setNewParams(ctx, data.Params) for _, validator := range data.Validators { - k.updateValidator(ctx, validator) + + // set validator + k.setValidator(ctx, validator) + + // manually set indexes for the first time + k.setValidatorByPubKeyIndex(ctx, validator) + k.setValidatorByPowerIndex(ctx, validator, data.Pool) + if validator.Status() == sdk.Bonded { + store.Set(GetValidatorsBondedKey(validator.PubKey), validator.Owner) + } } for _, bond := range data.Bonds { k.setDelegation(ctx, bond) } + k.updateBondedValidatorsFull(ctx, store) } // WriteGenesis - output genesis parameters diff --git a/x/stake/handler.go b/x/stake/handler.go index 53653557cc..6f2360adf7 100644 --- a/x/stake/handler.go +++ b/x/stake/handler.go @@ -55,6 +55,7 @@ func handleMsgDeclareCandidacy(ctx sdk.Context, msg MsgDeclareCandidacy, k Keepe validator := NewValidator(msg.ValidatorAddr, msg.PubKey, msg.Description) k.setValidator(ctx, validator) + k.setValidatorByPubKeyIndex(ctx, validator) tags := sdk.NewTags( "action", []byte("declareCandidacy"), "validator", msg.ValidatorAddr.Bytes(), diff --git a/x/stake/keeper.go b/x/stake/keeper.go index ce84b1e177..9fd902053a 100644 --- a/x/stake/keeper.go +++ b/x/stake/keeper.go @@ -8,6 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/wire" "github.com/cosmos/cosmos-sdk/x/bank" abci "github.com/tendermint/abci/types" + crypto "github.com/tendermint/go-crypto" ) // keeper of the staking store @@ -38,6 +39,16 @@ func (k Keeper) GetValidator(ctx sdk.Context, addr sdk.Address) (validator Valid return k.getValidator(store, addr) } +// get a single validator by pubkey +func (k Keeper) GetValidatorByPubKey(ctx sdk.Context, pubkey crypto.PubKey) (validator Validator, found bool) { + store := ctx.KVStore(k.storeKey) + addr := store.Get(GetValidatorByPubKeyIndexKey(pubkey)) + if addr == nil { + return validator, false + } + return k.getValidator(store, addr) +} + // get a single validator (reuse store) func (k Keeper) getValidator(store sdk.KVStore, addr sdk.Address) (validator Validator, found bool) { b := store.Get(GetValidatorKey(addr)) @@ -51,10 +62,22 @@ func (k Keeper) getValidator(store sdk.KVStore, addr sdk.Address) (validator Val // set the main record holding validator details func (k Keeper) setValidator(ctx sdk.Context, validator Validator) { store := ctx.KVStore(k.storeKey) + // set main store bz := k.cdc.MustMarshalBinary(validator) store.Set(GetValidatorKey(validator.Owner), bz) } +func (k Keeper) setValidatorByPubKeyIndex(ctx sdk.Context, validator Validator) { + store := ctx.KVStore(k.storeKey) + // set pointer by pubkey + store.Set(GetValidatorByPubKeyIndexKey(validator.PubKey), validator.Owner) +} + +func (k Keeper) setValidatorByPowerIndex(ctx sdk.Context, validator Validator, pool Pool) { + store := ctx.KVStore(k.storeKey) + store.Set(GetValidatorsByPowerKey(validator, pool), validator.Owner) +} + // Get the set of all validators with no limits, used during genesis dump func (k Keeper) getAllValidators(ctx sdk.Context) (validators Validators) { store := ctx.KVStore(k.storeKey) @@ -203,8 +226,12 @@ func (k Keeper) updateValidator(ctx sdk.Context, validator Validator) Validator oldValidator, oldFound := k.GetValidator(ctx, ownerAddr) if validator.Revoked && oldValidator.Status() == sdk.Bonded { - validator, pool = validator.UpdateStatus(pool, sdk.Unbonded) - k.setPool(ctx, pool) + validator = k.unbondValidator(ctx, store, validator) + + // need to also clear the cliff validator spot because the revoke has + // opened up a new spot which will be filled when + // updateValidatorsBonded is called + k.clearCliffValidator(ctx) } powerIncreasing := false @@ -409,7 +436,7 @@ func (k Keeper) updateBondedValidatorsFull(ctx sdk.Context, store sdk.KVStore) { } // perform all the store operations for when a validator status becomes unbonded -func (k Keeper) unbondValidator(ctx sdk.Context, store sdk.KVStore, validator Validator) { +func (k Keeper) unbondValidator(ctx sdk.Context, store sdk.KVStore, validator Validator) Validator { pool := k.GetPool(ctx) // sanity check @@ -431,6 +458,7 @@ func (k Keeper) unbondValidator(ctx sdk.Context, store sdk.KVStore, validator Va // also remove from the Bonded Validators Store store.Delete(GetValidatorsBondedKey(validator.PubKey)) + return validator } // perform all the store operations for when a validator status becomes bonded @@ -470,6 +498,7 @@ func (k Keeper) removeValidator(ctx sdk.Context, address sdk.Address) { store := ctx.KVStore(k.storeKey) pool := k.getPool(store) store.Delete(GetValidatorKey(address)) + store.Delete(GetValidatorByPubKeyIndexKey(validator.PubKey)) store.Delete(GetValidatorsByPowerKey(validator, pool)) // delete from the current and power weighted validator groups if the validator @@ -656,6 +685,13 @@ func (k Keeper) setCliffValidator(ctx sdk.Context, validator Validator, pool Poo store.Set(ValidatorCliffKey, validator.Owner) } +// clear the current validator and power of the validator on the cliff +func (k Keeper) clearCliffValidator(ctx sdk.Context) { + store := ctx.KVStore(k.storeKey) + store.Delete(ValidatorPowerCliffKey) + store.Delete(ValidatorCliffKey) +} + //__________________________________________________________________________ // Implements ValidatorSet @@ -749,3 +785,46 @@ func (k Keeper) IterateDelegators(ctx sdk.Context, delAddr sdk.Address, fn func( } iterator.Close() } + +// slash a validator +func (k Keeper) Slash(ctx sdk.Context, pubkey crypto.PubKey, height int64, fraction sdk.Rat) { + // TODO height ignored for now, see https://github.com/cosmos/cosmos-sdk/pull/1011#issuecomment-390253957 + logger := ctx.Logger().With("module", "x/stake") + val, found := k.GetValidatorByPubKey(ctx, pubkey) + if !found { + panic(fmt.Errorf("Attempted to slash a nonexistent validator with address %s", pubkey.Address())) + } + sharesToRemove := val.PoolShares.Amount.Mul(fraction) + pool := k.GetPool(ctx) + val, pool, burned := val.removePoolShares(pool, sharesToRemove) + k.setPool(ctx, pool) // update the pool + k.updateValidator(ctx, val) // update the validator, possibly kicking it out + logger.Info(fmt.Sprintf("Validator %s slashed by fraction %v, removed %v shares and burned %d tokens", pubkey.Address(), fraction, sharesToRemove, burned)) + return +} + +// revoke a validator +func (k Keeper) Revoke(ctx sdk.Context, pubkey crypto.PubKey) { + logger := ctx.Logger().With("module", "x/stake") + val, found := k.GetValidatorByPubKey(ctx, pubkey) + if !found { + panic(fmt.Errorf("Validator with pubkey %s not found, cannot revoke", pubkey)) + } + val.Revoked = true + k.updateValidator(ctx, val) // update the validator, now revoked + logger.Info(fmt.Sprintf("Validator %s revoked", pubkey.Address())) + return +} + +// unrevoke a validator +func (k Keeper) Unrevoke(ctx sdk.Context, pubkey crypto.PubKey) { + logger := ctx.Logger().With("module", "x/stake") + val, found := k.GetValidatorByPubKey(ctx, pubkey) + if !found { + panic(fmt.Errorf("Validator with pubkey %s not found, cannot unrevoke", pubkey)) + } + val.Revoked = false + k.updateValidator(ctx, val) // update the validator, now unrevoked + logger.Info(fmt.Sprintf("Validator %s unrevoked", pubkey.Address())) + return +} diff --git a/x/stake/keeper_keys.go b/x/stake/keeper_keys.go index 5a84d08f29..632a86ec3c 100644 --- a/x/stake/keeper_keys.go +++ b/x/stake/keeper_keys.go @@ -13,16 +13,17 @@ import ( //nolint var ( // Keys for store prefixes - ParamKey = []byte{0x00} // key for parameters relating to staking - PoolKey = []byte{0x01} // key for the staking pools - ValidatorsKey = []byte{0x02} // prefix for each key to a validator - ValidatorsBondedKey = []byte{0x03} // prefix for each key to bonded/actively validating validators - ValidatorsByPowerKey = []byte{0x04} // prefix for each key to a validator sorted by power - ValidatorCliffKey = []byte{0x05} // key for block-local tx index - ValidatorPowerCliffKey = []byte{0x06} // key for block-local tx index - TendermintUpdatesKey = []byte{0x07} // prefix for each key to a validator which is being updated - DelegationKey = []byte{0x08} // prefix for each key to a delegator's bond - IntraTxCounterKey = []byte{0x09} // key for block-local tx index + ParamKey = []byte{0x00} // key for parameters relating to staking + PoolKey = []byte{0x01} // key for the staking pools + ValidatorsKey = []byte{0x02} // prefix for each key to a validator + ValidatorsByPubKeyIndexKey = []byte{0x03} // prefix for each key to a validator by pubkey + ValidatorsBondedKey = []byte{0x04} // prefix for each key to bonded/actively validating validators + ValidatorsByPowerKey = []byte{0x05} // prefix for each key to a validator sorted by power + ValidatorCliffKey = []byte{0x06} // key for block-local tx index + ValidatorPowerCliffKey = []byte{0x07} // key for block-local tx index + TendermintUpdatesKey = []byte{0x08} // prefix for each key to a validator which is being updated + DelegationKey = []byte{0x09} // prefix for each key to a delegator's bond + IntraTxCounterKey = []byte{0x10} // key for block-local tx index ) const maxDigitsForAccount = 12 // ~220,000,000 atoms created at launch @@ -32,6 +33,11 @@ func GetValidatorKey(ownerAddr sdk.Address) []byte { return append(ValidatorsKey, ownerAddr.Bytes()...) } +// get the key for the validator with pubkey +func GetValidatorByPubKeyIndexKey(pubkey crypto.PubKey) []byte { + return append(ValidatorsByPubKeyIndexKey, pubkey.Bytes()...) +} + // get the key for the current validator group, ordered like tendermint func GetValidatorsBondedKey(pk crypto.PubKey) []byte { addr := pk.Address() diff --git a/x/stake/validator.go b/x/stake/validator.go index cf4ee85fda..729a605a96 100644 --- a/x/stake/validator.go +++ b/x/stake/validator.go @@ -47,6 +47,7 @@ func NewValidator(owner sdk.Address, pubKey crypto.PubKey, description Descripti return Validator{ Owner: owner, PubKey: pubKey, + Revoked: false, PoolShares: NewUnbondedShares(sdk.ZeroRat()), DelegatorShares: sdk.ZeroRat(), Description: description, @@ -154,6 +155,23 @@ func (v Validator) UpdateStatus(pool Pool, NewStatus sdk.BondStatus) (Validator, return v, pool } +// Remove pool shares +// Returns corresponding tokens, which could be burned (e.g. when slashing +// a validator) or redistributed elsewhere +func (v Validator) removePoolShares(pool Pool, poolShares sdk.Rat) (Validator, Pool, int64) { + var tokens int64 + switch v.Status() { + case sdk.Unbonded: + pool, tokens = pool.removeSharesUnbonded(poolShares) + case sdk.Unbonding: + pool, tokens = pool.removeSharesUnbonding(poolShares) + case sdk.Bonded: + pool, tokens = pool.removeSharesBonded(poolShares) + } + v.PoolShares.Amount = v.PoolShares.Amount.Sub(poolShares) + return v, pool, tokens +} + // XXX TEST // get the power or potential power for a validator // if bonded, the power is the BondedShares