diff --git a/CHANGELOG.md b/CHANGELOG.md index cd0ee7cee95a..d5ed05db7416 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,9 @@ deprecated and all components removed except the `legacy/` package. This require genesis state. Namely, `accounts` now exist under `app_state.auth.accounts`. The corresponding migration logic has been implemented for v0.38 target version. Applications can migrate via: `$ {appd} migrate v0.38 genesis.json`. +* (modules) [\#5299](https://github.com/cosmos/cosmos-sdk/pull/5299) Handling of `ABCIEvidenceTypeDuplicateVote` + during `BeginBlock` along with the corresponding parameters (`MaxEvidenceAge`) have moved from the + `x/slashing` module to the `x/evidence` module. ### API Breaking Changes @@ -71,6 +74,8 @@ if the provided arguments are invalid. * `StdTx#GetSignatures` will return an array of just signature byte slices `[][]byte` instead of returning an array of `StdSignature` structs. To replicate the old behavior, use the public field `StdTx.Signatures` to get back the array of StdSignatures `[]StdSignature`. +* (modules) [\#5299](https://github.com/cosmos/cosmos-sdk/pull/5299) `HandleDoubleSign` along with params `MaxEvidenceAge` + and `DoubleSignJailEndTime` have moved from the `x/slashing` module to the `x/evidence` module. ### Client Breaking Changes diff --git a/simapp/app.go b/simapp/app.go index 87e08e0c6858..4ed1a244dda1 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -194,6 +194,7 @@ func NewSimApp( // create evidence keeper with router evidenceKeeper := evidence.NewKeeper( app.cdc, keys[evidence.StoreKey], app.subspaces[evidence.ModuleName], evidence.DefaultCodespace, + &app.StakingKeeper, app.SlashingKeeper, ) evidenceRouter := evidence.NewRouter() // TODO: Register evidence routes. @@ -237,7 +238,7 @@ func NewSimApp( // During begin block slashing happens after distr.BeginBlocker so that // there is nothing left over in the validator fee pool, so as to keep the // CanWithdrawInvariant invariant. - app.mm.SetOrderBeginBlockers(upgrade.ModuleName, mint.ModuleName, distr.ModuleName, slashing.ModuleName) + app.mm.SetOrderBeginBlockers(upgrade.ModuleName, mint.ModuleName, distr.ModuleName, slashing.ModuleName, evidence.ModuleName) app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName) // NOTE: The genutils moodule must occur after staking so that pools are diff --git a/x/evidence/abci.go b/x/evidence/abci.go new file mode 100644 index 000000000000..e12ed7065f9b --- /dev/null +++ b/x/evidence/abci.go @@ -0,0 +1,25 @@ +package evidence + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + + abci "github.com/tendermint/tendermint/abci/types" + tmtypes "github.com/tendermint/tendermint/types" +) + +// BeginBlocker iterates through and handles any newly discovered evidence of +// misbehavior submitted by Tendermint. Currently, only equivocation is handled. +func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) { + for _, tmEvidence := range req.ByzantineValidators { + switch tmEvidence.Type { + case tmtypes.ABCIEvidenceTypeDuplicateVote: + evidence := ConvertDuplicateVoteEvidence(tmEvidence) + k.HandleDoubleSign(ctx, evidence.(Equivocation)) + + default: + k.Logger(ctx).Error(fmt.Sprintf("ignored unknown evidence type: %s", tmEvidence.Type)) + } + } +} diff --git a/x/evidence/alias.go b/x/evidence/alias.go index 5eca5a074f1f..411f39751578 100644 --- a/x/evidence/alias.go +++ b/x/evidence/alias.go @@ -15,6 +15,7 @@ const ( DefaultParamspace = types.DefaultParamspace QueryEvidence = types.QueryEvidence QueryAllEvidence = types.QueryAllEvidence + QueryParameters = types.QueryParameters CodeNoEvidenceHandlerExists = types.CodeNoEvidenceHandlerExists CodeInvalidEvidence = types.CodeInvalidEvidence CodeNoEvidenceExists = types.CodeNoEvidenceExists @@ -23,21 +24,26 @@ const ( EventTypeSubmitEvidence = types.EventTypeSubmitEvidence AttributeValueCategory = types.AttributeValueCategory AttributeKeyEvidenceHash = types.AttributeKeyEvidenceHash + DefaultMaxEvidenceAge = types.DefaultMaxEvidenceAge ) var ( NewKeeper = keeper.NewKeeper NewQuerier = keeper.NewQuerier - NewMsgSubmitEvidence = types.NewMsgSubmitEvidence - NewRouter = types.NewRouter - NewQueryEvidenceParams = types.NewQueryEvidenceParams - NewQueryAllEvidenceParams = types.NewQueryAllEvidenceParams - RegisterCodec = types.RegisterCodec - RegisterEvidenceTypeCodec = types.RegisterEvidenceTypeCodec - ModuleCdc = types.ModuleCdc - NewGenesisState = types.NewGenesisState - DefaultGenesisState = types.DefaultGenesisState + NewMsgSubmitEvidence = types.NewMsgSubmitEvidence + NewRouter = types.NewRouter + NewQueryEvidenceParams = types.NewQueryEvidenceParams + NewQueryAllEvidenceParams = types.NewQueryAllEvidenceParams + RegisterCodec = types.RegisterCodec + RegisterEvidenceTypeCodec = types.RegisterEvidenceTypeCodec + ModuleCdc = types.ModuleCdc + NewGenesisState = types.NewGenesisState + DefaultGenesisState = types.DefaultGenesisState + ConvertDuplicateVoteEvidence = types.ConvertDuplicateVoteEvidence + KeyMaxEvidenceAge = types.KeyMaxEvidenceAge + DoubleSignJailEndTime = types.DoubleSignJailEndTime + ParamKeyTable = types.ParamKeyTable ) type ( @@ -47,4 +53,5 @@ type ( MsgSubmitEvidence = types.MsgSubmitEvidence Handler = types.Handler Router = types.Router + Equivocation = types.Equivocation ) diff --git a/x/evidence/client/cli/query.go b/x/evidence/client/cli/query.go index 0529af859411..4c5eb13abaf1 100644 --- a/x/evidence/client/cli/query.go +++ b/x/evidence/client/cli/query.go @@ -46,7 +46,38 @@ $ %s query %s --page=2 --limit=50 cmd.Flags().Int(flagPage, 1, "pagination page of evidence to to query for") cmd.Flags().Int(flagLimit, 100, "pagination limit of evidence to query for") - return cmd + cmd.AddCommand(client.GetCommands(QueryParamsCmd(cdc))...) + + return client.GetCommands(cmd)[0] +} + +// QueryParamsCmd returns the command handler for evidence parameter querying. +func QueryParamsCmd(cdc *codec.Codec) *cobra.Command { + return &cobra.Command{ + Use: "params", + Short: "Query the current evidence parameters", + Args: cobra.NoArgs, + Long: strings.TrimSpace(`Query the current evidence parameters: + +$ query evidence params +`), + RunE: func(cmd *cobra.Command, args []string) error { + cliCtx := context.NewCLIContext().WithCodec(cdc) + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryParameters) + res, _, err := cliCtx.QueryWithData(route, nil) + if err != nil { + return err + } + + var params types.Params + if err := cdc.UnmarshalJSON(res, ¶ms); err != nil { + return fmt.Errorf("failed to unmarshal params: %w", err) + } + + return cliCtx.PrintOutput(params) + }, + } } // QueryEvidenceCmd returns the command handler for evidence querying. Evidence diff --git a/x/evidence/client/rest/query.go b/x/evidence/client/rest/query.go index 84a894487a99..45b66660ac73 100644 --- a/x/evidence/client/rest/query.go +++ b/x/evidence/client/rest/query.go @@ -22,6 +22,11 @@ func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) { "/evidence", queryAllEvidenceHandler(cliCtx), ).Methods(MethodGet) + + r.HandleFunc( + "/evidence/params", + queryParamsHandler(cliCtx), + ).Methods(MethodGet) } func queryEvidenceHandler(cliCtx context.CLIContext) http.HandlerFunc { @@ -89,3 +94,22 @@ func queryAllEvidenceHandler(cliCtx context.CLIContext) http.HandlerFunc { rest.PostProcessResponse(w, cliCtx, res) } } + +func queryParamsHandler(cliCtx context.CLIContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + route := fmt.Sprintf("custom/%s/%s", types.QuerierRoute, types.QueryParameters) + res, height, err := cliCtx.QueryWithData(route, nil) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + } +} diff --git a/x/evidence/genesis.go b/x/evidence/genesis.go index b06c7ce96783..2ec9e954202e 100644 --- a/x/evidence/genesis.go +++ b/x/evidence/genesis.go @@ -20,11 +20,14 @@ func InitGenesis(ctx sdk.Context, k Keeper, gs GenesisState) { k.SetEvidence(ctx, e) } + + k.SetParams(ctx, gs.Params) } // ExportGenesis returns the evidence module's exported genesis. func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState { return GenesisState{ + Params: k.GetParams(ctx), Evidence: k.GetAllEvidence(ctx), } } diff --git a/x/evidence/genesis_test.go b/x/evidence/genesis_test.go index 3f3e3abac48b..a77b847db177 100644 --- a/x/evidence/genesis_test.go +++ b/x/evidence/genesis_test.go @@ -33,7 +33,7 @@ func (suite *GenesisTestSuite) SetupTest() { // recreate keeper in order to use custom testing types evidenceKeeper := evidence.NewKeeper( cdc, app.GetKey(evidence.StoreKey), app.GetSubspace(evidence.ModuleName), - evidence.DefaultCodespace, + evidence.DefaultCodespace, app.StakingKeeper, app.SlashingKeeper, ) router := evidence.NewRouter() router = router.AddRoute(types.TestEvidenceRouteEquivocation, types.TestEquivocationHandler(*evidenceKeeper)) @@ -67,7 +67,7 @@ func (suite *GenesisTestSuite) TestInitGenesis_Valid() { } suite.NotPanics(func() { - evidence.InitGenesis(suite.ctx, suite.keeper, evidence.NewGenesisState(testEvidence)) + evidence.InitGenesis(suite.ctx, suite.keeper, evidence.NewGenesisState(types.DefaultParams(), testEvidence)) }) for _, e := range testEvidence { @@ -100,7 +100,7 @@ func (suite *GenesisTestSuite) TestInitGenesis_Invalid() { } suite.Panics(func() { - evidence.InitGenesis(suite.ctx, suite.keeper, evidence.NewGenesisState(testEvidence)) + evidence.InitGenesis(suite.ctx, suite.keeper, evidence.NewGenesisState(types.DefaultParams(), testEvidence)) }) suite.Empty(suite.keeper.GetAllEvidence(suite.ctx)) diff --git a/x/evidence/handler_test.go b/x/evidence/handler_test.go index 0b286817ee2c..1957778950ad 100644 --- a/x/evidence/handler_test.go +++ b/x/evidence/handler_test.go @@ -32,7 +32,7 @@ func (suite *HandlerTestSuite) SetupTest() { // recreate keeper in order to use custom testing types evidenceKeeper := evidence.NewKeeper( cdc, app.GetKey(evidence.StoreKey), app.GetSubspace(evidence.ModuleName), - evidence.DefaultCodespace, + evidence.DefaultCodespace, app.StakingKeeper, app.SlashingKeeper, ) router := evidence.NewRouter() router = router.AddRoute(types.TestEvidenceRouteEquivocation, types.TestEquivocationHandler(*evidenceKeeper)) diff --git a/x/evidence/internal/keeper/infraction.go b/x/evidence/internal/keeper/infraction.go new file mode 100644 index 000000000000..fab16c833e74 --- /dev/null +++ b/x/evidence/internal/keeper/infraction.go @@ -0,0 +1,109 @@ +package keeper + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" +) + +// HandleDoubleSign implements an equivocation evidence handler. Assuming the +// evidence is valid, the validator committing the misbehavior will be slashed, +// jailed and tombstoned. Once tombstoned, the validator will not be able to +// recover. Note, the evidence contains the block time and height at the time of +// the equivocation. +// +// The evidence is considered invalid if: +// - the evidence is too old +// - the validator is unbonded or does not exist +// - the signing info does not exist (will panic) +// - is already tombstoned +// +// TODO: Some of the invalid constraints listed above may need to be reconsidered +// in the case of a lunatic attack. +func (k Keeper) HandleDoubleSign(ctx sdk.Context, evidence types.Equivocation) { + logger := k.Logger(ctx) + consAddr := evidence.GetConsensusAddress() + infractionHeight := evidence.GetHeight() + + // calculate the age of the evidence + blockTime := ctx.BlockHeader().Time + age := blockTime.Sub(evidence.GetTime()) + + if _, err := k.slashingKeeper.GetPubkey(ctx, consAddr.Bytes()); err != nil { + // Ignore evidence that cannot be handled. + // + // NOTE: We used to panic with: + // `panic(fmt.Sprintf("Validator consensus-address %v not found", consAddr))`, + // but this couples the expectations of the app to both Tendermint and + // the simulator. Both are expected to provide the full range of + // allowable but none of the disallowed evidence types. Instead of + // getting this coordination right, it is easier to relax the + // constraints and ignore evidence that cannot be handled. + return + } + + // reject evidence if the double-sign is too old + if age > k.MaxEvidenceAge(ctx) { + logger.Info( + fmt.Sprintf( + "ignored double sign from %s at height %d, age of %d past max age of %d", + consAddr, infractionHeight, age, k.MaxEvidenceAge(ctx), + ), + ) + return + } + + validator := k.stakingKeeper.ValidatorByConsAddr(ctx, consAddr) + if validator == nil || validator.IsUnbonded() { + // Defensive: Simulation doesn't take unbonding periods into account, and + // Tendermint might break this assumption at some point. + return + } + + if ok := k.slashingKeeper.HasValidatorSigningInfo(ctx, consAddr); !ok { + panic(fmt.Sprintf("expected signing info for validator %s but not found", consAddr)) + } + + // ignore if the validator is already tombstoned + if k.slashingKeeper.IsTombstoned(ctx, consAddr) { + logger.Info( + fmt.Sprintf( + "ignored double sign from %s at height %d, validator already tombstoned", + consAddr, infractionHeight, + ), + ) + return + } + + logger.Info(fmt.Sprintf("confirmed double sign from %s at height %d, age of %d", consAddr, infractionHeight, age)) + + // We need to retrieve the stake distribution which signed the block, so we + // subtract ValidatorUpdateDelay from the evidence height. + // Note, that this *can* result in a negative "distributionHeight", up to + // -ValidatorUpdateDelay, i.e. at the end of the + // pre-genesis block (none) = at the beginning of the genesis block. + // That's fine since this is just used to filter unbonding delegations & redelegations. + distributionHeight := infractionHeight - sdk.ValidatorUpdateDelay + + // Slash validator. The `power` is the int64 power of the validator as provided + // to/by Tendermint. This value is validator.Tokens as sent to Tendermint via + // ABCI, and now received as evidence. The fraction is passed in to separately + // to slash unbonding and rebonding delegations. + k.slashingKeeper.Slash( + ctx, + consAddr, + k.slashingKeeper.SlashFractionDoubleSign(ctx), + evidence.GetValidatorPower(), distributionHeight, + ) + + // Jail the validator if not already jailed. This will begin unbonding the + // validator if not already unbonding (tombstoned). + if !validator.IsJailed() { + k.slashingKeeper.Jail(ctx, consAddr) + } + + k.slashingKeeper.JailUntil(ctx, consAddr, types.DoubleSignJailEndTime) + k.slashingKeeper.Tombstone(ctx, consAddr) +} diff --git a/x/evidence/internal/keeper/infraction_test.go b/x/evidence/internal/keeper/infraction_test.go new file mode 100644 index 000000000000..ca68948f5eb1 --- /dev/null +++ b/x/evidence/internal/keeper/infraction_test.go @@ -0,0 +1,117 @@ +package keeper_test + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" + "github.com/cosmos/cosmos-sdk/x/staking" + + "github.com/tendermint/tendermint/crypto" +) + +func newTestMsgCreateValidator(address sdk.ValAddress, pubKey crypto.PubKey, amt sdk.Int) staking.MsgCreateValidator { + commission := staking.NewCommissionRates(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()) + return staking.NewMsgCreateValidator( + address, pubKey, sdk.NewCoin(sdk.DefaultBondDenom, amt), + staking.Description{}, commission, sdk.OneInt(), + ) +} + +func (suite *KeeperTestSuite) TestHandleDoubleSign() { + ctx := suite.ctx.WithIsCheckTx(false).WithBlockHeight(1) + suite.populateValidators(ctx) + + power := int64(100) + stakingParams := suite.app.StakingKeeper.GetParams(ctx) + amt := sdk.TokensFromConsensusPower(power) + operatorAddr, val := valAddresses[0], pubkeys[0] + + // create validator + res := staking.NewHandler(suite.app.StakingKeeper)(ctx, newTestMsgCreateValidator(operatorAddr, val, amt)) + suite.True(res.IsOK(), res.Log) + + // execute end-blocker and verify validator attributes + staking.EndBlocker(ctx, suite.app.StakingKeeper) + suite.Equal( + suite.app.BankKeeper.GetCoins(ctx, sdk.AccAddress(operatorAddr)), + sdk.NewCoins(sdk.NewCoin(stakingParams.BondDenom, initAmt.Sub(amt))), + ) + suite.Equal(amt, suite.app.StakingKeeper.Validator(ctx, operatorAddr).GetBondedTokens()) + + // handle a signature to set signing info + suite.app.SlashingKeeper.HandleValidatorSignature(ctx, val.Address(), amt.Int64(), true) + + // double sign less than max age + oldTokens := suite.app.StakingKeeper.Validator(ctx, operatorAddr).GetTokens() + evidence := types.Equivocation{ + Height: 0, + Time: time.Unix(0, 0), + Power: power, + ConsensusAddress: sdk.ConsAddress(val.Address()), + } + suite.keeper.HandleDoubleSign(ctx, evidence) + + // should be jailed and tombstoned + suite.True(suite.app.StakingKeeper.Validator(ctx, operatorAddr).IsJailed()) + suite.True(suite.app.SlashingKeeper.IsTombstoned(ctx, sdk.ConsAddress(val.Address()))) + + // tokens should be decreased + newTokens := suite.app.StakingKeeper.Validator(ctx, operatorAddr).GetTokens() + suite.True(newTokens.LT(oldTokens)) + + // submit duplicate evidence + suite.keeper.HandleDoubleSign(ctx, evidence) + + // tokens should be the same (capped slash) + suite.True(suite.app.StakingKeeper.Validator(ctx, operatorAddr).GetTokens().Equal(newTokens)) + + // jump to past the unbonding period + ctx = ctx.WithBlockTime(time.Unix(1, 0).Add(stakingParams.UnbondingTime)) + + // require we cannot unjail + suite.Error(suite.app.SlashingKeeper.Unjail(ctx, operatorAddr)) + + // require we be able to unbond now + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1) + del, _ := suite.app.StakingKeeper.GetDelegation(ctx, sdk.AccAddress(operatorAddr), operatorAddr) + validator, _ := suite.app.StakingKeeper.GetValidator(ctx, operatorAddr) + totalBond := validator.TokensFromShares(del.GetShares()).TruncateInt() + msgUnbond := staking.NewMsgUndelegate(sdk.AccAddress(operatorAddr), operatorAddr, sdk.NewCoin(stakingParams.BondDenom, totalBond)) + res = staking.NewHandler(suite.app.StakingKeeper)(ctx, msgUnbond) + suite.True(res.IsOK()) +} + +func (suite *KeeperTestSuite) TestHandleDoubleSign_TooOld() { + ctx := suite.ctx.WithIsCheckTx(false).WithBlockHeight(1).WithBlockTime(time.Now()) + suite.populateValidators(ctx) + + power := int64(100) + stakingParams := suite.app.StakingKeeper.GetParams(ctx) + amt := sdk.TokensFromConsensusPower(power) + operatorAddr, val := valAddresses[0], pubkeys[0] + + // create validator + res := staking.NewHandler(suite.app.StakingKeeper)(ctx, newTestMsgCreateValidator(operatorAddr, val, amt)) + suite.True(res.IsOK(), res.Log) + + // execute end-blocker and verify validator attributes + staking.EndBlocker(ctx, suite.app.StakingKeeper) + suite.Equal( + suite.app.BankKeeper.GetCoins(ctx, sdk.AccAddress(operatorAddr)), + sdk.NewCoins(sdk.NewCoin(stakingParams.BondDenom, initAmt.Sub(amt))), + ) + suite.Equal(amt, suite.app.StakingKeeper.Validator(ctx, operatorAddr).GetBondedTokens()) + + evidence := types.Equivocation{ + Height: 0, + Time: ctx.BlockTime(), + Power: power, + ConsensusAddress: sdk.ConsAddress(val.Address()), + } + ctx = ctx.WithBlockTime(ctx.BlockTime().Add(suite.app.EvidenceKeeper.MaxEvidenceAge(ctx) + 1)) + suite.keeper.HandleDoubleSign(ctx, evidence) + + suite.False(suite.app.StakingKeeper.Validator(ctx, operatorAddr).IsJailed()) + suite.False(suite.app.SlashingKeeper.IsTombstoned(ctx, sdk.ConsAddress(val.Address()))) +} diff --git a/x/evidence/internal/keeper/keeper.go b/x/evidence/internal/keeper/keeper.go index fa79aac7be08..26b45db7d69d 100644 --- a/x/evidence/internal/keeper/keeper.go +++ b/x/evidence/internal/keeper/keeper.go @@ -18,22 +18,32 @@ import ( // managing persistence, state transitions and query handling for the evidence // module. type Keeper struct { - cdc *codec.Codec - storeKey sdk.StoreKey - paramSpace params.Subspace - router types.Router - codespace sdk.CodespaceType + cdc *codec.Codec + storeKey sdk.StoreKey + paramSpace params.Subspace + router types.Router + stakingKeeper types.StakingKeeper + slashingKeeper types.SlashingKeeper + codespace sdk.CodespaceType } func NewKeeper( cdc *codec.Codec, storeKey sdk.StoreKey, paramSpace params.Subspace, codespace sdk.CodespaceType, + stakingKeeper types.StakingKeeper, slashingKeeper types.SlashingKeeper, ) *Keeper { + // set KeyTable if it has not already been set + if !paramSpace.HasKeyTable() { + paramSpace = paramSpace.WithKeyTable(types.ParamKeyTable()) + } + return &Keeper{ - cdc: cdc, - storeKey: storeKey, - paramSpace: paramSpace, - codespace: codespace, + cdc: cdc, + storeKey: storeKey, + paramSpace: paramSpace, + stakingKeeper: stakingKeeper, + slashingKeeper: slashingKeeper, + codespace: codespace, } } diff --git a/x/evidence/internal/keeper/keeper_test.go b/x/evidence/internal/keeper/keeper_test.go index 2f7862283ae8..bde9f08f02ce 100644 --- a/x/evidence/internal/keeper/keeper_test.go +++ b/x/evidence/internal/keeper/keeper_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "encoding/hex" "testing" "github.com/cosmos/cosmos-sdk/simapp" @@ -9,18 +10,50 @@ import ( "github.com/cosmos/cosmos-sdk/x/evidence/exported" "github.com/cosmos/cosmos-sdk/x/evidence/internal/keeper" "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" + "github.com/cosmos/cosmos-sdk/x/supply" "github.com/stretchr/testify/suite" abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" ) +var ( + pubkeys = []crypto.PubKey{ + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB50"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB51"), + newPubKey("0B485CFC0EECC619440448436F8FC9DF40566F2369E72400281454CB552AFB52"), + } + + valAddresses = []sdk.ValAddress{ + sdk.ValAddress(pubkeys[0].Address()), + sdk.ValAddress(pubkeys[1].Address()), + sdk.ValAddress(pubkeys[2].Address()), + } + + initAmt = sdk.TokensFromConsensusPower(200) + initCoins = sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, initAmt)) +) + +func newPubKey(pk string) (res crypto.PubKey) { + pkBytes, err := hex.DecodeString(pk) + if err != nil { + panic(err) + } + + var pubkey ed25519.PubKeyEd25519 + copy(pubkey[:], pkBytes) + + return pubkey +} + type KeeperTestSuite struct { suite.Suite ctx sdk.Context querier sdk.Querier keeper keeper.Keeper + app *simapp.SimApp } func (suite *KeeperTestSuite) SetupTest() { @@ -34,7 +67,7 @@ func (suite *KeeperTestSuite) SetupTest() { // recreate keeper in order to use custom testing types evidenceKeeper := evidence.NewKeeper( cdc, app.GetKey(evidence.StoreKey), app.GetSubspace(evidence.ModuleName), - evidence.DefaultCodespace, + evidence.DefaultCodespace, app.StakingKeeper, app.SlashingKeeper, ) router := evidence.NewRouter() router = router.AddRoute(types.TestEvidenceRouteEquivocation, types.TestEquivocationHandler(*evidenceKeeper)) @@ -43,6 +76,7 @@ func (suite *KeeperTestSuite) SetupTest() { suite.ctx = app.BaseApp.NewContext(checkTx, abci.Header{Height: 1}) suite.querier = keeper.NewQuerier(*evidenceKeeper) suite.keeper = *evidenceKeeper + suite.app = app } func (suite *KeeperTestSuite) populateEvidence(ctx sdk.Context, numEvidence int) []exported.Evidence { @@ -74,6 +108,18 @@ func (suite *KeeperTestSuite) populateEvidence(ctx sdk.Context, numEvidence int) return evidence } +func (suite *KeeperTestSuite) populateValidators(ctx sdk.Context) { + // add accounts and set total supply + totalSupplyAmt := initAmt.MulRaw(int64(len(valAddresses))) + totalSupply := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, totalSupplyAmt)) + suite.app.SupplyKeeper.SetSupply(ctx, supply.NewSupply(totalSupply)) + + for _, addr := range valAddresses { + _, err := suite.app.BankKeeper.AddCoins(ctx, sdk.AccAddress(addr), initCoins) + suite.NoError(err) + } +} + func (suite *KeeperTestSuite) TestSubmitValidEvidence() { ctx := suite.ctx.WithIsCheckTx(false) pk := ed25519.GenPrivKey() diff --git a/x/evidence/internal/keeper/params.go b/x/evidence/internal/keeper/params.go new file mode 100644 index 000000000000..8db4867781e7 --- /dev/null +++ b/x/evidence/internal/keeper/params.go @@ -0,0 +1,25 @@ +package keeper + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" +) + +// MaxEvidenceAge returns the maximum age for submitted evidence. +func (k Keeper) MaxEvidenceAge(ctx sdk.Context) (res time.Duration) { + k.paramSpace.Get(ctx, types.KeyMaxEvidenceAge, &res) + return +} + +// GetParams returns the total set of evidence parameters. +func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) { + k.paramSpace.GetParamSet(ctx, ¶ms) + return params +} + +// SetParams sets the evidence parameters to the param space. +func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { + k.paramSpace.SetParamSet(ctx, ¶ms) +} diff --git a/x/evidence/internal/keeper/params_test.go b/x/evidence/internal/keeper/params_test.go new file mode 100644 index 000000000000..58d25230eec7 --- /dev/null +++ b/x/evidence/internal/keeper/params_test.go @@ -0,0 +1,11 @@ +package keeper_test + +import ( + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" +) + +func (suite *KeeperTestSuite) TestParams() { + ctx := suite.ctx.WithIsCheckTx(false) + suite.Equal(types.DefaultParams(), suite.keeper.GetParams(ctx)) + suite.Equal(types.DefaultMaxEvidenceAge, suite.keeper.MaxEvidenceAge(ctx)) +} diff --git a/x/evidence/internal/keeper/querier.go b/x/evidence/internal/keeper/querier.go index fa6ef17c0f31..dfbda68f66d6 100644 --- a/x/evidence/internal/keeper/querier.go +++ b/x/evidence/internal/keeper/querier.go @@ -21,11 +21,14 @@ func NewQuerier(k Keeper) sdk.Querier { ) switch path[0] { + case types.QueryParameters: + res, err = queryParams(ctx, k) + case types.QueryEvidence: - res, err = queryEvidence(ctx, path[1:], req, k) + res, err = queryEvidence(ctx, req, k) case types.QueryAllEvidence: - res, err = queryAllEvidence(ctx, path[1:], req, k) + res, err = queryAllEvidence(ctx, req, k) default: err = sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName) @@ -35,7 +38,18 @@ func NewQuerier(k Keeper) sdk.Querier { } } -func queryEvidence(ctx sdk.Context, _ []string, req abci.RequestQuery, k Keeper) ([]byte, error) { +func queryParams(ctx sdk.Context, k Keeper) ([]byte, error) { + params := k.GetParams(ctx) + + res, err := codec.MarshalJSONIndent(k.cdc, params) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + + return res, nil +} + +func queryEvidence(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) { var params types.QueryEvidenceParams err := k.cdc.UnmarshalJSON(req.Data, ¶ms) @@ -61,7 +75,7 @@ func queryEvidence(ctx sdk.Context, _ []string, req abci.RequestQuery, k Keeper) return res, nil } -func queryAllEvidence(ctx sdk.Context, _ []string, req abci.RequestQuery, k Keeper) ([]byte, error) { +func queryAllEvidence(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) { var params types.QueryAllEvidenceParams err := k.cdc.UnmarshalJSON(req.Data, ¶ms) diff --git a/x/evidence/internal/keeper/querier_test.go b/x/evidence/internal/keeper/querier_test.go index b656019edd8a..68af96640ab4 100644 --- a/x/evidence/internal/keeper/querier_test.go +++ b/x/evidence/internal/keeper/querier_test.go @@ -84,3 +84,12 @@ func (suite *KeeperTestSuite) TestQueryAllEvidence_InvalidPagination() { suite.Nil(types.TestingCdc.UnmarshalJSON(bz, &e)) suite.Len(e, 0) } + +func (suite *KeeperTestSuite) TestQueryParams() { + ctx := suite.ctx.WithIsCheckTx(false) + + bz, err := suite.querier(ctx, []string{types.QueryParameters}, abci.RequestQuery{}) + suite.Nil(err) + suite.NotNil(bz) + suite.Equal("{\n \"max_evidence_age\": \"120000000000\"\n}", string(bz)) +} diff --git a/x/evidence/internal/types/codec.go b/x/evidence/internal/types/codec.go index 72fb044e9d55..7c57bc4a71f8 100644 --- a/x/evidence/internal/types/codec.go +++ b/x/evidence/internal/types/codec.go @@ -14,6 +14,7 @@ var ModuleCdc = codec.New() func RegisterCodec(cdc *codec.Codec) { cdc.RegisterInterface((*exported.Evidence)(nil), nil) cdc.RegisterConcrete(MsgSubmitEvidence{}, "cosmos-sdk/MsgSubmitEvidence", nil) + cdc.RegisterConcrete(Equivocation{}, "cosmos-sdk/Equivocation", nil) } // RegisterEvidenceTypeCodec registers an external concrete Evidence type defined diff --git a/x/evidence/internal/types/evidence.go b/x/evidence/internal/types/evidence.go new file mode 100644 index 000000000000..d4dbfc1eafef --- /dev/null +++ b/x/evidence/internal/types/evidence.go @@ -0,0 +1,101 @@ +package types + +import ( + "fmt" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/evidence/exported" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/tmhash" + cmn "github.com/tendermint/tendermint/libs/common" + "gopkg.in/yaml.v2" +) + +// Evidence type constants +const ( + RouteEquivocation = "equivocation" + TypeEquivocation = "equivocation" +) + +var _ exported.Evidence = (*Equivocation)(nil) + +// Equivocation implements the Evidence interface and defines evidence of double +// signing misbehavior. +type Equivocation struct { + Height int64 `json:"height" yaml:"height"` + Time time.Time `json:"time" yaml:"time"` + Power int64 `json:"power" yaml:"power"` + ConsensusAddress sdk.ConsAddress `json:"consensus_address" yaml:"consensus_address"` +} + +// Route returns the Evidence Handler route for an Equivocation type. +func (e Equivocation) Route() string { return RouteEquivocation } + +// Type returns the Evidence Handler type for an Equivocation type. +func (e Equivocation) Type() string { return TypeEquivocation } + +func (e Equivocation) String() string { + bz, _ := yaml.Marshal(e) + return string(bz) +} + +// Hash returns the hash of an Equivocation object. +func (e Equivocation) Hash() cmn.HexBytes { + return tmhash.Sum(ModuleCdc.MustMarshalBinaryBare(e)) +} + +// ValidateBasic performs basic stateless validation checks on an Equivocation object. +func (e Equivocation) ValidateBasic() error { + if e.Time.IsZero() { + return fmt.Errorf("invalid equivocation time: %s", e.Time) + } + if e.Height < 1 { + return fmt.Errorf("invalid equivocation height: %d", e.Height) + } + if e.Power < 1 { + return fmt.Errorf("invalid equivocation validator power: %d", e.Power) + } + if e.ConsensusAddress.Empty() { + return fmt.Errorf("invalid equivocation validator consensus address: %s", e.ConsensusAddress) + } + + return nil +} + +// GetConsensusAddress returns the validator's consensus address at time of the +// Equivocation infraction. +func (e Equivocation) GetConsensusAddress() sdk.ConsAddress { + return e.ConsensusAddress +} + +// GetHeight returns the height at time of the Equivocation infraction. +func (e Equivocation) GetHeight() int64 { + return e.Height +} + +// GetTime returns the time at time of the Equivocation infraction. +func (e Equivocation) GetTime() time.Time { + return e.Time +} + +// GetValidatorPower returns the validator's power at time of the Equivocation +// infraction. +func (e Equivocation) GetValidatorPower() int64 { + return e.Power +} + +// GetTotalPower is a no-op for the Equivocation type. +func (e Equivocation) GetTotalPower() int64 { return 0 } + +// ConvertDuplicateVoteEvidence converts a Tendermint concrete Evidence type to +// SDK Evidence using Equivocation as the concrete type. +func ConvertDuplicateVoteEvidence(dupVote abci.Evidence) exported.Evidence { + return Equivocation{ + Height: dupVote.Height, + Power: dupVote.Validator.Power, + ConsensusAddress: sdk.ConsAddress(dupVote.Validator.Address), + Time: dupVote.Time, + } +} diff --git a/x/evidence/internal/types/evidence_test.go b/x/evidence/internal/types/evidence_test.go new file mode 100644 index 000000000000..ad2107b8fa41 --- /dev/null +++ b/x/evidence/internal/types/evidence_test.go @@ -0,0 +1,55 @@ +package types_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/evidence/internal/types" + "github.com/stretchr/testify/require" +) + +func TestEquivocation_Valid(t *testing.T) { + n, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") + e := types.Equivocation{ + Height: 100, + Time: n, + Power: 1000000, + ConsensusAddress: sdk.ConsAddress("foo"), + } + + require.Equal(t, e.GetTotalPower(), int64(0)) + require.Equal(t, e.GetValidatorPower(), e.Power) + require.Equal(t, e.GetTime(), e.Time) + require.Equal(t, e.GetConsensusAddress(), e.ConsensusAddress) + require.Equal(t, e.GetHeight(), e.Height) + require.Equal(t, e.Type(), types.TypeEquivocation) + require.Equal(t, e.Route(), types.RouteEquivocation) + require.Equal(t, e.Hash().String(), "808DA679674C9C0599965D02EBC5D4DCFD5E700D03035BBCD2DECCBBF44386F7") + require.Equal(t, e.String(), "height: 100\ntime: 2006-01-02T15:04:05Z\npower: 1000000\nconsensus_address: cosmosvalcons1vehk7pqt5u4\n") + require.NoError(t, e.ValidateBasic()) +} + +func TestEquivocationValidateBasic(t *testing.T) { + var zeroTime time.Time + + n, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z") + testCases := []struct { + name string + e types.Equivocation + expectErr bool + }{ + {"valid", types.Equivocation{100, n, 1000000, sdk.ConsAddress("foo")}, false}, + {"invalid time", types.Equivocation{100, zeroTime, 1000000, sdk.ConsAddress("foo")}, true}, + {"invalid height", types.Equivocation{0, n, 1000000, sdk.ConsAddress("foo")}, true}, + {"invalid power", types.Equivocation{100, n, 0, sdk.ConsAddress("foo")}, true}, + {"invalid address", types.Equivocation{100, n, 1000000, nil}, true}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expectErr, tc.e.ValidateBasic() != nil) + }) + } +} diff --git a/x/evidence/internal/types/expected_keepers.go b/x/evidence/internal/types/expected_keepers.go new file mode 100644 index 000000000000..fc4b12247639 --- /dev/null +++ b/x/evidence/internal/types/expected_keepers.go @@ -0,0 +1,31 @@ +package types + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported" + + "github.com/tendermint/tendermint/crypto" +) + +type ( + // StakingKeeper defines the staking module interface contract needed by the + // evidence module. + StakingKeeper interface { + ValidatorByConsAddr(sdk.Context, sdk.ConsAddress) stakingexported.ValidatorI + } + + // SlashingKeeper defines the slashing module interface contract needed by the + // evidence module. + SlashingKeeper interface { + GetPubkey(sdk.Context, crypto.Address) (crypto.PubKey, error) + IsTombstoned(sdk.Context, sdk.ConsAddress) bool + HasValidatorSigningInfo(sdk.Context, sdk.ConsAddress) bool + Tombstone(sdk.Context, sdk.ConsAddress) + Slash(sdk.Context, sdk.ConsAddress, sdk.Dec, int64, int64) + SlashFractionDoubleSign(sdk.Context) sdk.Dec + Jail(sdk.Context, sdk.ConsAddress) + JailUntil(sdk.Context, sdk.ConsAddress, time.Time) + } +) diff --git a/x/evidence/internal/types/genesis.go b/x/evidence/internal/types/genesis.go index 4013b573fc9f..83c5a0f10a58 100644 --- a/x/evidence/internal/types/genesis.go +++ b/x/evidence/internal/types/genesis.go @@ -1,21 +1,33 @@ package types -import "github.com/cosmos/cosmos-sdk/x/evidence/exported" +import ( + "fmt" + "time" + + "github.com/cosmos/cosmos-sdk/x/evidence/exported" +) // DONTCOVER // GenesisState defines the evidence module's genesis state. type GenesisState struct { + Params Params `json:"params" yaml:"params"` Evidence []exported.Evidence `json:"evidence" yaml:"evidence"` } -func NewGenesisState(e []exported.Evidence) GenesisState { - return GenesisState{Evidence: e} +func NewGenesisState(p Params, e []exported.Evidence) GenesisState { + return GenesisState{ + Params: p, + Evidence: e, + } } // DefaultGenesisState returns the evidence module's default genesis state. func DefaultGenesisState() GenesisState { - return GenesisState{Evidence: []exported.Evidence{}} + return GenesisState{ + Params: DefaultParams(), + Evidence: []exported.Evidence{}, + } } // Validate performs basic gensis state validation returning an error upon any @@ -27,5 +39,10 @@ func (gs GenesisState) Validate() error { } } + maxEvidence := gs.Params.MaxEvidenceAge + if maxEvidence < 1*time.Minute { + return fmt.Errorf("max evidence age must be at least 1 minute, is %s", maxEvidence.String()) + } + return nil } diff --git a/x/evidence/internal/types/genesis_test.go b/x/evidence/internal/types/genesis_test.go index 936877ad3c2a..f7ef8027b25c 100644 --- a/x/evidence/internal/types/genesis_test.go +++ b/x/evidence/internal/types/genesis_test.go @@ -39,7 +39,7 @@ func TestGenesisStateValidate_Valid(t *testing.T) { } } - gs := types.NewGenesisState(evidence) + gs := types.NewGenesisState(types.DefaultParams(), evidence) require.NoError(t, gs.Validate()) } @@ -66,6 +66,6 @@ func TestGenesisStateValidate_Invalid(t *testing.T) { } } - gs := types.NewGenesisState(evidence) + gs := types.NewGenesisState(types.DefaultParams(), evidence) require.Error(t, gs.Validate()) } diff --git a/x/evidence/internal/types/keys.go b/x/evidence/internal/types/keys.go index 702f76d9b64a..775fa2c021a1 100644 --- a/x/evidence/internal/types/keys.go +++ b/x/evidence/internal/types/keys.go @@ -12,9 +12,6 @@ const ( // QuerierRoute defines the module's query routing key QuerierRoute = ModuleName - - // DefaultParamspace defines the module's default paramspace name - DefaultParamspace = ModuleName ) // KVStore key prefixes diff --git a/x/evidence/internal/types/params.go b/x/evidence/internal/types/params.go new file mode 100644 index 000000000000..5684786b73f7 --- /dev/null +++ b/x/evidence/internal/types/params.go @@ -0,0 +1,60 @@ +package types + +import ( + "time" + + "github.com/cosmos/cosmos-sdk/x/params" + + "gopkg.in/yaml.v2" +) + +// DONTCOVER + +// Default parameter values +const ( + DefaultParamspace = ModuleName + DefaultMaxEvidenceAge = 60 * 2 * time.Second +) + +// Parameter store keys +var ( + KeyMaxEvidenceAge = []byte("MaxEvidenceAge") + + // The Double Sign Jail period ends at Max Time supported by Amino + // (Dec 31, 9999 - 23:59:59 GMT). + DoubleSignJailEndTime = time.Unix(253402300799, 0) +) + +// Params defines the total set of parameters for the evidence module +type Params struct { + MaxEvidenceAge time.Duration `json:"max_evidence_age" yaml:"max_evidence_age"` +} + +// ParamKeyTable returns the parameter key table. +func ParamKeyTable() params.KeyTable { + return params.NewKeyTable().RegisterParamSet(&Params{}) +} + +func (p Params) MarshalYAML() (interface{}, error) { + bz, err := yaml.Marshal(p) + return string(bz), err +} + +func (p Params) String() string { + out, _ := p.MarshalYAML() + return out.(string) +} + +// ParamSetPairs returns the parameter set pairs. +func (p *Params) ParamSetPairs() params.ParamSetPairs { + return params.ParamSetPairs{ + params.NewParamSetPair(KeyMaxEvidenceAge, &p.MaxEvidenceAge), + } +} + +// DefaultParams returns the default parameters for the evidence module. +func DefaultParams() Params { + return Params{ + MaxEvidenceAge: DefaultMaxEvidenceAge, + } +} diff --git a/x/evidence/internal/types/querier.go b/x/evidence/internal/types/querier.go index af643ee7c276..130d0699de45 100644 --- a/x/evidence/internal/types/querier.go +++ b/x/evidence/internal/types/querier.go @@ -2,6 +2,7 @@ package types // Querier routes for the evidence module const ( + QueryParameters = "parameters" QueryEvidence = "evidence" QueryAllEvidence = "all_evidence" ) diff --git a/x/evidence/module.go b/x/evidence/module.go index a66a35adf36c..aa4129469dab 100644 --- a/x/evidence/module.go +++ b/x/evidence/module.go @@ -158,7 +158,9 @@ func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage { } // BeginBlock executes all ABCI BeginBlock logic respective to the evidence module. -func (AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {} +func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { + BeginBlocker(ctx, req, am.keeper) +} // EndBlock executes all ABCI EndBlock logic respective to the evidence module. It // returns no validator updates. diff --git a/x/evidence/spec/05_params.md b/x/evidence/spec/05_params.md new file mode 100644 index 000000000000..ff30153725c1 --- /dev/null +++ b/x/evidence/spec/05_params.md @@ -0,0 +1,7 @@ +# Parameters + +The evidence module contains the following parameters: + +| Key | Type | Example | +| -------------- | ---------------- | -------------- | +| MaxEvidenceAge | string (time ns) | "120000000000" | diff --git a/x/evidence/spec/06_begin_block.md b/x/evidence/spec/06_begin_block.md new file mode 100644 index 000000000000..312e2b0e6d3d --- /dev/null +++ b/x/evidence/spec/06_begin_block.md @@ -0,0 +1,96 @@ +# BeginBlock + +## Evidence Handling + +Tendermint blocks can include +[Evidence](https://github.com/tendermint/tendermint/blob/master/docs/spec/blockchain/blockchain.md#evidence), +which indicates that a validator committed malicious behavior. The relevant information is +forwarded to the application as ABCI Evidence in `abci.RequestBeginBlock` so that +the validator an be accordingly punished. + +### Equivocation + +Currently, the evidence module only handles evidence of type `Equivocation` which is derived from +Tendermint's `ABCIEvidenceTypeDuplicateVote` during `BeginBlock`. + +For some `Equivocation` submitted in `block` to be valid, it must satisfy: + +`Evidence.Timestamp >= block.Timestamp - MaxEvidenceAge` + +Where `Evidence.Timestamp` is the timestamp in the block at height `Evidence.Height` and +`block.Timestamp` is the current block timestamp. + +If valid `Equivocation` evidence is included in a block, the validator's stake is +reduced (slashed) by `SlashFractionDoubleSign`, which is defined by the `x/slashing` module, +of what their stake was when the infraction occurred (rather than when the evidence was discovered). +We want to "follow the stake", i.e. the stake which contributed to the infraction +should be slashed, even if it has since been redelegated or started unbonding. + +In addition, the validator is permanently jailed and tombstoned making it impossible for that +validator to ever re-enter the validator set. + +The `Equivocation` evidence is handled as follows: + +```go +func (k Keeper) HandleDoubleSign(ctx Context, evidence Equivocation) { + consAddr := evidence.GetConsensusAddress() + infractionHeight := evidence.GetHeight() + + // calculate the age of the evidence + blockTime := ctx.BlockHeader().Time + age := blockTime.Sub(evidence.GetTime()) + + // reject evidence we cannot handle + if _, err := k.slashingKeeper.GetPubkey(ctx, consAddr.Bytes()); err != nil { + return + } + + // reject evidence if it is too old + if age > k.MaxEvidenceAge(ctx) { + return + } + + // reject evidence if the validator is already unbonded + validator := k.stakingKeeper.ValidatorByConsAddr(ctx, consAddr) + if validator == nil || validator.IsUnbonded() { + return + } + + // verify the validator has signing info in order to be slashed and tombstoned + if ok := k.slashingKeeper.HasValidatorSigningInfo(ctx, consAddr); !ok { + panic(...) + } + + // reject evidence if the validator is already tombstoned + if k.slashingKeeper.IsTombstoned(ctx, consAddr) { + return + } + + // We need to retrieve the stake distribution which signed the block, so we + // subtract ValidatorUpdateDelay from the evidence height. + // Note, that this *can* result in a negative "distributionHeight", up to + // -ValidatorUpdateDelay, i.e. at the end of the + // pre-genesis block (none) = at the beginning of the genesis block. + // That's fine since this is just used to filter unbonding delegations & redelegations. + distributionHeight := infractionHeight - sdk.ValidatorUpdateDelay + + // Slash validator. The `power` is the int64 power of the validator as provided + // to/by Tendermint. This value is validator.Tokens as sent to Tendermint via + // ABCI, and now received as evidence. The fraction is passed in to separately + // to slash unbonding and rebonding delegations. + k.slashingKeeper.Slash(ctx, consAddr, evidence.GetValidatorPower(), distributionHeight) + + // Jail the validator if not already jailed. This will begin unbonding the + // validator if not already unbonding (tombstoned). + if !validator.IsJailed() { + k.slashingKeeper.Jail(ctx, consAddr) + } + + k.slashingKeeper.JailUntil(ctx, consAddr, types.DoubleSignJailEndTime) + k.slashingKeeper.Tombstone(ctx, consAddr) +} +``` + +Note, the slashing, jailing, and tombstoning calls are delegated through the `x/slashing` module +which emit informative events and finally delegate calls to the `x/staking` module. Documentation +on slashing and jailing can be found in the [x/staking spec](/.././cosmos-sdk/x/staking/spec/02_state_transitions.md) diff --git a/x/evidence/spec/README.md b/x/evidence/spec/README.md index 1f8667b1b7f0..9313dc7616ad 100644 --- a/x/evidence/spec/README.md +++ b/x/evidence/spec/README.md @@ -1,5 +1,15 @@ # Evidence Module Specification +## Table of Contents + + +1. **[Concepts](01_concepts.md)** +2. **[State](02_state.md)** +3. **[Messages](03_messages.md)** +4. **[Events](04_events.md)** +5. **[Params](05_params.md)** +6. **[BeginBlock](06_begin_block.md)** + ## Abstract `x/evidence` is an implementation of a Cosmos SDK module, per [ADR 009](./../../../docs/architecture/adr-009-evidence-module.md), @@ -20,9 +30,3 @@ keeper in order for it to be successfully routed and executed. Each corresponding handler must also fulfill the `Handler` interface contract. The `Handler` for a given `Evidence` type can perform any arbitrary state transitions such as slashing, jailing, and tombstoning. - - -1. **[Concepts](01_concepts.md)** -2. **[State](02_state.md)** -3. **[Messages](03_messages.md)** -4. **[Events](04_events.md)** diff --git a/x/params/keeper_test.go b/x/params/keeper_test.go index 6f66e7fe8e14..7b94204a6207 100644 --- a/x/params/keeper_test.go +++ b/x/params/keeper_test.go @@ -39,7 +39,10 @@ func TestKeeper(t *testing.T) { cdc, ctx, skey, _, keeper := testComponents() store := prefix.NewStore(ctx.KVStore(skey), []byte("test/")) - space := keeper.Subspace("test").WithKeyTable(table) + space := keeper.Subspace("test") + require.False(t, space.HasKeyTable()) + space = space.WithKeyTable(table) + require.True(t, space.HasKeyTable()) // Set params for i, kv := range kvs { diff --git a/x/params/subspace/subspace.go b/x/params/subspace/subspace.go index 673e27e71055..aee65842035b 100644 --- a/x/params/subspace/subspace.go +++ b/x/params/subspace/subspace.go @@ -46,6 +46,11 @@ func NewSubspace(cdc *codec.Codec, key sdk.StoreKey, tkey sdk.StoreKey, name str return } +// HasKeyTable returns if the Subspace has a KeyTable registered. +func (s Subspace) HasKeyTable() bool { + return len(s.table.m) > 0 +} + // WithKeyTable initializes KeyTable and returns modified Subspace func (s Subspace) WithKeyTable(table KeyTable) Subspace { if table.m == nil { diff --git a/x/slashing/abci.go b/x/slashing/abci.go index 74931763f850..c6caeec9b5fd 100644 --- a/x/slashing/abci.go +++ b/x/slashing/abci.go @@ -1,10 +1,7 @@ package slashing import ( - "fmt" - abci "github.com/tendermint/tendermint/abci/types" - tmtypes "github.com/tendermint/tendermint/types" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -18,16 +15,4 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) { for _, voteInfo := range req.LastCommitInfo.GetVotes() { k.HandleValidatorSignature(ctx, voteInfo.Validator.Address, voteInfo.Validator.Power, voteInfo.SignedLastBlock) } - - // Iterate through any newly discovered evidence of infraction - // Slash any validators (and since-unbonded stake within the unbonding period) - // who contributed to valid infractions - for _, evidence := range req.ByzantineValidators { - switch evidence.Type { - case tmtypes.ABCIEvidenceTypeDuplicateVote: - k.HandleDoubleSign(ctx, evidence.Validator.Address, evidence.Height, evidence.Time, evidence.Validator.Power) - default: - k.Logger(ctx).Error(fmt.Sprintf("ignored unknown evidence type: %s", evidence.Type)) - } - } } diff --git a/x/slashing/alias.go b/x/slashing/alias.go index c0107510336e..677228182c0d 100644 --- a/x/slashing/alias.go +++ b/x/slashing/alias.go @@ -23,7 +23,6 @@ const ( RouterKey = types.RouterKey QuerierRoute = types.QuerierRoute DefaultParamspace = types.DefaultParamspace - DefaultMaxEvidenceAge = types.DefaultMaxEvidenceAge DefaultSignedBlocksWindow = types.DefaultSignedBlocksWindow DefaultDowntimeJailDuration = types.DefaultDowntimeJailDuration QueryParameters = types.QueryParameters @@ -77,11 +76,9 @@ var ( ValidatorSigningInfoKey = types.ValidatorSigningInfoKey ValidatorMissedBlockBitArrayKey = types.ValidatorMissedBlockBitArrayKey AddrPubkeyRelationKey = types.AddrPubkeyRelationKey - DoubleSignJailEndTime = types.DoubleSignJailEndTime DefaultMinSignedPerWindow = types.DefaultMinSignedPerWindow DefaultSlashFractionDoubleSign = types.DefaultSlashFractionDoubleSign DefaultSlashFractionDowntime = types.DefaultSlashFractionDowntime - KeyMaxEvidenceAge = types.KeyMaxEvidenceAge KeySignedBlocksWindow = types.KeySignedBlocksWindow KeyMinSignedPerWindow = types.KeyMinSignedPerWindow KeyDowntimeJailDuration = types.KeyDowntimeJailDuration diff --git a/x/slashing/internal/keeper/infractions.go b/x/slashing/internal/keeper/infractions.go index e71ccb7142a8..a10b6f945b3a 100644 --- a/x/slashing/internal/keeper/infractions.go +++ b/x/slashing/internal/keeper/infractions.go @@ -2,7 +2,6 @@ package keeper import ( "fmt" - "time" "github.com/tendermint/tendermint/crypto" @@ -10,107 +9,6 @@ import ( "github.com/cosmos/cosmos-sdk/x/slashing/internal/types" ) -// HandleDoubleSign handles a validator signing two blocks at the same height. -// power: power of the double-signing validator at the height of infraction -func (k Keeper) HandleDoubleSign(ctx sdk.Context, addr crypto.Address, infractionHeight int64, timestamp time.Time, power int64) { - logger := k.Logger(ctx) - - // calculate the age of the evidence - time := ctx.BlockHeader().Time - age := time.Sub(timestamp) - - // fetch the validator public key - consAddr := sdk.ConsAddress(addr) - if _, err := k.GetPubkey(ctx, addr); err != nil { - // Ignore evidence that cannot be handled. - // NOTE: - // We used to panic with: - // `panic(fmt.Sprintf("Validator consensus-address %v not found", consAddr))`, - // but this couples the expectations of the app to both Tendermint and - // the simulator. Both are expected to provide the full range of - // allowable but none of the disallowed evidence types. Instead of - // getting this coordination right, it is easier to relax the - // constraints and ignore evidence that cannot be handled. - return - } - - // Reject evidence if the double-sign is too old - if age > k.MaxEvidenceAge(ctx) { - logger.Info(fmt.Sprintf("Ignored double sign from %s at height %d, age of %d past max age of %d", - consAddr, infractionHeight, age, k.MaxEvidenceAge(ctx))) - return - } - - // Get validator and signing info - validator := k.sk.ValidatorByConsAddr(ctx, consAddr) - if validator == nil || validator.IsUnbonded() { - // Defensive. - // Simulation doesn't take unbonding periods into account, and - // Tendermint might break this assumption at some point. - return - } - - // fetch the validator signing info - signInfo, found := k.GetValidatorSigningInfo(ctx, consAddr) - if !found { - panic(fmt.Sprintf("Expected signing info for validator %s but not found", consAddr)) - } - - // validator is already tombstoned - if signInfo.Tombstoned { - logger.Info(fmt.Sprintf("Ignored double sign from %s at height %d, validator already tombstoned", consAddr, infractionHeight)) - return - } - - // double sign confirmed - logger.Info(fmt.Sprintf("Confirmed double sign from %s at height %d, age of %d", consAddr, infractionHeight, age)) - - // We need to retrieve the stake distribution which signed the block, so we subtract ValidatorUpdateDelay from the evidence height. - // Note that this *can* result in a negative "distributionHeight", up to -ValidatorUpdateDelay, - // i.e. at the end of the pre-genesis block (none) = at the beginning of the genesis block. - // That's fine since this is just used to filter unbonding delegations & redelegations. - distributionHeight := infractionHeight - sdk.ValidatorUpdateDelay - - // get the percentage slash penalty fraction - fraction := k.SlashFractionDoubleSign(ctx) - - // Slash validator - // `power` is the int64 power of the validator as provided to/by - // Tendermint. This value is validator.Tokens as sent to Tendermint via - // ABCI, and now received as evidence. - // The fraction is passed in to separately to slash unbonding and rebonding delegations. - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeSlash, - sdk.NewAttribute(types.AttributeKeyAddress, consAddr.String()), - sdk.NewAttribute(types.AttributeKeyPower, fmt.Sprintf("%d", power)), - sdk.NewAttribute(types.AttributeKeyReason, types.AttributeValueDoubleSign), - ), - ) - k.sk.Slash(ctx, consAddr, distributionHeight, power, fraction) - - // Jail validator if not already jailed - // begin unbonding validator if not already unbonding (tombstone) - if !validator.IsJailed() { - ctx.EventManager().EmitEvent( - sdk.NewEvent( - types.EventTypeSlash, - sdk.NewAttribute(types.AttributeKeyJailed, consAddr.String()), - ), - ) - k.sk.Jail(ctx, consAddr) - } - - // Set tombstoned to be true - signInfo.Tombstoned = true - - // Set jailed until to be forever (max time) - signInfo.JailedUntil = types.DoubleSignJailEndTime - - // Set validator signing info - k.SetValidatorSigningInfo(ctx, consAddr, signInfo) -} - // HandleValidatorSignature handles a validator signature, must be called once per validator per block. func (k Keeper) HandleValidatorSignature(ctx sdk.Context, addr crypto.Address, power int64, signed bool) { logger := k.Logger(ctx) diff --git a/x/slashing/internal/keeper/keeper.go b/x/slashing/internal/keeper/keeper.go index 4b4d28900308..b7a3706369f9 100644 --- a/x/slashing/internal/keeper/keeper.go +++ b/x/slashing/internal/keeper/keeper.go @@ -54,6 +54,34 @@ func (k Keeper) GetPubkey(ctx sdk.Context, address crypto.Address) (crypto.PubKe return pubkey, nil } +// Slash attempts to slash a validator. The slash is delegated to the staking +// module to make the necessary validator changes. +func (k Keeper) Slash(ctx sdk.Context, consAddr sdk.ConsAddress, fraction sdk.Dec, power, distributionHeight int64) { + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeSlash, + sdk.NewAttribute(types.AttributeKeyAddress, consAddr.String()), + sdk.NewAttribute(types.AttributeKeyPower, fmt.Sprintf("%d", power)), + sdk.NewAttribute(types.AttributeKeyReason, types.AttributeValueDoubleSign), + ), + ) + + k.sk.Slash(ctx, consAddr, distributionHeight, power, fraction) +} + +// Jail attempts to jail a validator. The slash is delegated to the staking module +// to make the necessary validator changes. +func (k Keeper) Jail(ctx sdk.Context, consAddr sdk.ConsAddress) { + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeSlash, + sdk.NewAttribute(types.AttributeKeyJailed, consAddr.String()), + ), + ) + + k.sk.Jail(ctx, consAddr) +} + func (k Keeper) setAddrPubkeyRelation(ctx sdk.Context, addr crypto.Address, pubkey crypto.PubKey) { store := ctx.KVStore(k.storeKey) bz := k.cdc.MustMarshalBinaryLengthPrefixed(pubkey) diff --git a/x/slashing/internal/keeper/keeper_test.go b/x/slashing/internal/keeper/keeper_test.go index 36b67c7bbd6c..54b77848a043 100644 --- a/x/slashing/internal/keeper/keeper_test.go +++ b/x/slashing/internal/keeper/keeper_test.go @@ -5,111 +5,12 @@ import ( "time" "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/slashing/internal/types" "github.com/cosmos/cosmos-sdk/x/staking" ) -// ______________________________________________________________ - -// Test that a validator is slashed correctly -// when we discover evidence of infraction -func TestHandleDoubleSign(t *testing.T) { - - // initial setup - ctx, ck, sk, _, keeper := CreateTestInput(t, TestParams()) - // validator added pre-genesis - ctx = ctx.WithBlockHeight(-1) - power := int64(100) - amt := sdk.TokensFromConsensusPower(power) - operatorAddr, val := Addrs[0], Pks[0] - got := staking.NewHandler(sk)(ctx, NewTestMsgCreateValidator(operatorAddr, val, amt)) - require.True(t, got.IsOK()) - staking.EndBlocker(ctx, sk) - require.Equal( - t, ck.GetCoins(ctx, sdk.AccAddress(operatorAddr)), - sdk.NewCoins(sdk.NewCoin(sk.GetParams(ctx).BondDenom, InitTokens.Sub(amt))), - ) - require.Equal(t, amt, sk.Validator(ctx, operatorAddr).GetBondedTokens()) - - // handle a signature to set signing info - keeper.HandleValidatorSignature(ctx, val.Address(), amt.Int64(), true) - - oldTokens := sk.Validator(ctx, operatorAddr).GetTokens() - - // double sign less than max age - keeper.HandleDoubleSign(ctx, val.Address(), 0, time.Unix(0, 0), power) - - // should be jailed - require.True(t, sk.Validator(ctx, operatorAddr).IsJailed()) - - // tokens should be decreased - newTokens := sk.Validator(ctx, operatorAddr).GetTokens() - require.True(t, newTokens.LT(oldTokens)) - - // New evidence - keeper.HandleDoubleSign(ctx, val.Address(), 0, time.Unix(0, 0), power) - - // tokens should be the same (capped slash) - require.True(t, sk.Validator(ctx, operatorAddr).GetTokens().Equal(newTokens)) - - // Jump to past the unbonding period - ctx = ctx.WithBlockHeader(abci.Header{Time: time.Unix(1, 0).Add(sk.GetParams(ctx).UnbondingTime)}) - - // Still shouldn't be able to unjail - require.Error(t, keeper.Unjail(ctx, operatorAddr)) - - // Should be able to unbond now - del, _ := sk.GetDelegation(ctx, sdk.AccAddress(operatorAddr), operatorAddr) - validator, _ := sk.GetValidator(ctx, operatorAddr) - - totalBond := validator.TokensFromShares(del.GetShares()).TruncateInt() - msgUnbond := staking.NewMsgUndelegate(sdk.AccAddress(operatorAddr), operatorAddr, sdk.NewCoin(sk.GetParams(ctx).BondDenom, totalBond)) - res := staking.NewHandler(sk)(ctx, msgUnbond) - require.True(t, res.IsOK()) -} - -// ______________________________________________________________ - -// Test that a validator is slashed correctly -// when we discover evidence of infraction -func TestPastMaxEvidenceAge(t *testing.T) { - - // initial setup - ctx, ck, sk, _, keeper := CreateTestInput(t, TestParams()) - // validator added pre-genesis - ctx = ctx.WithBlockHeight(-1) - power := int64(100) - amt := sdk.TokensFromConsensusPower(power) - operatorAddr, val := Addrs[0], Pks[0] - got := staking.NewHandler(sk)(ctx, NewTestMsgCreateValidator(operatorAddr, val, amt)) - require.True(t, got.IsOK()) - staking.EndBlocker(ctx, sk) - require.Equal( - t, ck.GetCoins(ctx, sdk.AccAddress(operatorAddr)), - sdk.NewCoins(sdk.NewCoin(sk.GetParams(ctx).BondDenom, InitTokens.Sub(amt))), - ) - require.Equal(t, amt, sk.Validator(ctx, operatorAddr).GetBondedTokens()) - - // handle a signature to set signing info - keeper.HandleValidatorSignature(ctx, val.Address(), power, true) - - ctx = ctx.WithBlockHeader(abci.Header{Time: time.Unix(1, 0).Add(keeper.MaxEvidenceAge(ctx))}) - - oldPower := sk.Validator(ctx, operatorAddr).GetConsensusPower() - - // double sign past max age - keeper.HandleDoubleSign(ctx, val.Address(), 0, time.Unix(0, 0), power) - - // should still be bonded - require.True(t, sk.Validator(ctx, operatorAddr).IsBonded()) - - // should still have same power - require.Equal(t, oldPower, sk.Validator(ctx, operatorAddr).GetConsensusPower()) -} - // Test a new validator entering the validator set // Ensure that SigningInfo.StartHeight is set correctly // and that they are not immediately jailed diff --git a/x/slashing/internal/keeper/params.go b/x/slashing/internal/keeper/params.go index 124588dc7f6c..82fe991cccc1 100644 --- a/x/slashing/internal/keeper/params.go +++ b/x/slashing/internal/keeper/params.go @@ -7,12 +7,6 @@ import ( "github.com/cosmos/cosmos-sdk/x/slashing/internal/types" ) -// MaxEvidenceAge - max age for evidence -func (k Keeper) MaxEvidenceAge(ctx sdk.Context) (res time.Duration) { - k.paramspace.Get(ctx, types.KeyMaxEvidenceAge, &res) - return -} - // SignedBlocksWindow - sliding window for downtime slashing func (k Keeper) SignedBlocksWindow(ctx sdk.Context) (res int64) { k.paramspace.Get(ctx, types.KeySignedBlocksWindow, &res) diff --git a/x/slashing/internal/keeper/signing_info.go b/x/slashing/internal/keeper/signing_info.go index ebb61e732880..1162c258f35a 100644 --- a/x/slashing/internal/keeper/signing_info.go +++ b/x/slashing/internal/keeper/signing_info.go @@ -1,6 +1,8 @@ package keeper import ( + "time" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/slashing/internal/types" ) @@ -19,6 +21,13 @@ func (k Keeper) GetValidatorSigningInfo(ctx sdk.Context, address sdk.ConsAddress return } +// HasValidatorSigningInfo returns if a given validator has signing information +// persited. +func (k Keeper) HasValidatorSigningInfo(ctx sdk.Context, consAddr sdk.ConsAddress) bool { + _, ok := k.GetValidatorSigningInfo(ctx, consAddr) + return ok +} + // SetValidatorSigningInfo sets the validator signing info to a consensus address key func (k Keeper) SetValidatorSigningInfo(ctx sdk.Context, address sdk.ConsAddress, info types.ValidatorSigningInfo) { store := ctx.KVStore(k.storeKey) @@ -77,6 +86,44 @@ func (k Keeper) IterateValidatorMissedBlockBitArray(ctx sdk.Context, } } +// JailUntil attempts to set a validator's JailedUntil attribute in its signing +// info. It will panic if the signing info does not exist for the validator. +func (k Keeper) JailUntil(ctx sdk.Context, consAddr sdk.ConsAddress, jailTime time.Time) { + signInfo, ok := k.GetValidatorSigningInfo(ctx, consAddr) + if !ok { + panic("cannot jail validator that does not have any signing information") + } + + signInfo.JailedUntil = jailTime + k.SetValidatorSigningInfo(ctx, consAddr, signInfo) +} + +// Tombstone attempts to tombstone a validator. It will panic if signing info for +// the given validator does not exist. +func (k Keeper) Tombstone(ctx sdk.Context, consAddr sdk.ConsAddress) { + signInfo, ok := k.GetValidatorSigningInfo(ctx, consAddr) + if !ok { + panic("cannot tombstone validator that does not have any signing information") + } + + if signInfo.Tombstoned { + panic("cannot tombstone validator that is already tombstoned") + } + + signInfo.Tombstoned = true + k.SetValidatorSigningInfo(ctx, consAddr, signInfo) +} + +// IsTombstoned returns if a given validator by consensus address is tombstoned. +func (k Keeper) IsTombstoned(ctx sdk.Context, consAddr sdk.ConsAddress) bool { + signInfo, ok := k.GetValidatorSigningInfo(ctx, consAddr) + if !ok { + return false + } + + return signInfo.Tombstoned +} + // SetValidatorMissedBlockBitArray sets the bit that checks if the validator has // missed a block in the current window func (k Keeper) SetValidatorMissedBlockBitArray(ctx sdk.Context, address sdk.ConsAddress, index int64, missed bool) { diff --git a/x/slashing/internal/keeper/signing_info_test.go b/x/slashing/internal/keeper/signing_info_test.go index e7d302f48194..7d9e7c56e526 100644 --- a/x/slashing/internal/keeper/signing_info_test.go +++ b/x/slashing/internal/keeper/signing_info_test.go @@ -39,3 +39,44 @@ func TestGetSetValidatorMissedBlockBitArray(t *testing.T) { missed = keeper.GetValidatorMissedBlockBitArray(ctx, sdk.ConsAddress(Addrs[0]), 0) require.True(t, missed) // now should be missed } + +func TestTombstoned(t *testing.T) { + ctx, _, _, _, keeper := CreateTestInput(t, types.DefaultParams()) + require.Panics(t, func() { keeper.Tombstone(ctx, sdk.ConsAddress(Addrs[0])) }) + require.False(t, keeper.IsTombstoned(ctx, sdk.ConsAddress(Addrs[0]))) + + newInfo := types.NewValidatorSigningInfo( + sdk.ConsAddress(Addrs[0]), + int64(4), + int64(3), + time.Unix(2, 0), + false, + int64(10), + ) + keeper.SetValidatorSigningInfo(ctx, sdk.ConsAddress(Addrs[0]), newInfo) + + require.False(t, keeper.IsTombstoned(ctx, sdk.ConsAddress(Addrs[0]))) + keeper.Tombstone(ctx, sdk.ConsAddress(Addrs[0])) + require.True(t, keeper.IsTombstoned(ctx, sdk.ConsAddress(Addrs[0]))) + require.Panics(t, func() { keeper.Tombstone(ctx, sdk.ConsAddress(Addrs[0])) }) +} + +func TestJailUntil(t *testing.T) { + ctx, _, _, _, keeper := CreateTestInput(t, types.DefaultParams()) + require.Panics(t, func() { keeper.JailUntil(ctx, sdk.ConsAddress(Addrs[0]), time.Now()) }) + + newInfo := types.NewValidatorSigningInfo( + sdk.ConsAddress(Addrs[0]), + int64(4), + int64(3), + time.Unix(2, 0), + false, + int64(10), + ) + keeper.SetValidatorSigningInfo(ctx, sdk.ConsAddress(Addrs[0]), newInfo) + keeper.JailUntil(ctx, sdk.ConsAddress(Addrs[0]), time.Unix(253402300799, 0).UTC()) + + info, ok := keeper.GetValidatorSigningInfo(ctx, sdk.ConsAddress(Addrs[0])) + require.True(t, ok) + require.Equal(t, time.Unix(253402300799, 0).UTC(), info.JailedUntil) +} diff --git a/x/slashing/internal/types/genesis.go b/x/slashing/internal/types/genesis.go index 4aadb9aaaf6d..42089eea5191 100644 --- a/x/slashing/internal/types/genesis.go +++ b/x/slashing/internal/types/genesis.go @@ -66,11 +66,6 @@ func ValidateGenesis(data GenesisState) error { return fmt.Errorf("min signed per window should be less than or equal to one and greater than zero, is %s", minSign.String()) } - maxEvidence := data.Params.MaxEvidenceAge - if maxEvidence < 1*time.Minute { - return fmt.Errorf("max evidence age must be at least 1 minute, is %s", maxEvidence.String()) - } - downtimeJail := data.Params.DowntimeJailDuration if downtimeJail < 1*time.Minute { return fmt.Errorf("downtime unblond duration must be at least 1 minute, is %s", downtimeJail.String()) diff --git a/x/slashing/internal/types/params.go b/x/slashing/internal/types/params.go index 20892dd8224f..4817ac5ee3ef 100644 --- a/x/slashing/internal/types/params.go +++ b/x/slashing/internal/types/params.go @@ -11,14 +11,11 @@ import ( // Default parameter namespace const ( DefaultParamspace = ModuleName - DefaultMaxEvidenceAge = 60 * 2 * time.Second DefaultSignedBlocksWindow = int64(100) DefaultDowntimeJailDuration = 60 * 10 * time.Second ) -// The Double Sign Jail period ends at Max Time supported by Amino (Dec 31, 9999 - 23:59:59 GMT) var ( - DoubleSignJailEndTime = time.Unix(253402300799, 0) DefaultMinSignedPerWindow = sdk.NewDecWithPrec(5, 1) DefaultSlashFractionDoubleSign = sdk.NewDec(1).Quo(sdk.NewDec(20)) DefaultSlashFractionDowntime = sdk.NewDec(1).Quo(sdk.NewDec(100)) @@ -26,7 +23,6 @@ var ( // Parameter store keys var ( - KeyMaxEvidenceAge = []byte("MaxEvidenceAge") KeySignedBlocksWindow = []byte("SignedBlocksWindow") KeyMinSignedPerWindow = []byte("MinSignedPerWindow") KeyDowntimeJailDuration = []byte("DowntimeJailDuration") @@ -41,7 +37,6 @@ func ParamKeyTable() params.KeyTable { // Params - used for initializing default parameter for slashing at genesis type Params struct { - MaxEvidenceAge time.Duration `json:"max_evidence_age" yaml:"max_evidence_age"` SignedBlocksWindow int64 `json:"signed_blocks_window" yaml:"signed_blocks_window"` MinSignedPerWindow sdk.Dec `json:"min_signed_per_window" yaml:"min_signed_per_window"` DowntimeJailDuration time.Duration `json:"downtime_jail_duration" yaml:"downtime_jail_duration"` @@ -50,12 +45,12 @@ type Params struct { } // NewParams creates a new Params object -func NewParams(maxEvidenceAge time.Duration, signedBlocksWindow int64, - minSignedPerWindow sdk.Dec, downtimeJailDuration time.Duration, - slashFractionDoubleSign, slashFractionDowntime sdk.Dec) Params { +func NewParams( + signedBlocksWindow int64, minSignedPerWindow sdk.Dec, downtimeJailDuration time.Duration, + slashFractionDoubleSign, slashFractionDowntime sdk.Dec, +) Params { return Params{ - MaxEvidenceAge: maxEvidenceAge, SignedBlocksWindow: signedBlocksWindow, MinSignedPerWindow: minSignedPerWindow, DowntimeJailDuration: downtimeJailDuration, @@ -67,12 +62,11 @@ func NewParams(maxEvidenceAge time.Duration, signedBlocksWindow int64, // String implements the stringer interface for Params func (p Params) String() string { return fmt.Sprintf(`Slashing Params: - MaxEvidenceAge: %s SignedBlocksWindow: %d MinSignedPerWindow: %s DowntimeJailDuration: %s SlashFractionDoubleSign: %s - SlashFractionDowntime: %s`, p.MaxEvidenceAge, + SlashFractionDowntime: %s`, p.SignedBlocksWindow, p.MinSignedPerWindow, p.DowntimeJailDuration, p.SlashFractionDoubleSign, p.SlashFractionDowntime) @@ -81,7 +75,6 @@ func (p Params) String() string { // ParamSetPairs - Implements params.ParamSet func (p *Params) ParamSetPairs() params.ParamSetPairs { return params.ParamSetPairs{ - params.NewParamSetPair(KeyMaxEvidenceAge, &p.MaxEvidenceAge), params.NewParamSetPair(KeySignedBlocksWindow, &p.SignedBlocksWindow), params.NewParamSetPair(KeyMinSignedPerWindow, &p.MinSignedPerWindow), params.NewParamSetPair(KeyDowntimeJailDuration, &p.DowntimeJailDuration), @@ -93,7 +86,7 @@ func (p *Params) ParamSetPairs() params.ParamSetPairs { // DefaultParams defines the parameters for this module func DefaultParams() Params { return NewParams( - DefaultMaxEvidenceAge, DefaultSignedBlocksWindow, DefaultMinSignedPerWindow, - DefaultDowntimeJailDuration, DefaultSlashFractionDoubleSign, DefaultSlashFractionDowntime, + DefaultSignedBlocksWindow, DefaultMinSignedPerWindow, DefaultDowntimeJailDuration, + DefaultSlashFractionDoubleSign, DefaultSlashFractionDowntime, ) } diff --git a/x/slashing/simulation/genesis.go b/x/slashing/simulation/genesis.go index dfe9e813fe16..a26a626642b7 100644 --- a/x/slashing/simulation/genesis.go +++ b/x/slashing/simulation/genesis.go @@ -82,8 +82,8 @@ func RandomizedGenState(simState *module.SimulationState) { ) params := types.NewParams( - simState.UnbondTime, signedBlocksWindow, minSignedPerWindow, - downtimeJailDuration, slashFractionDoubleSign, slashFractionDowntime, + signedBlocksWindow, minSignedPerWindow, downtimeJailDuration, + slashFractionDoubleSign, slashFractionDowntime, ) slashingGenesis := types.NewGenesisState(params, nil, nil) diff --git a/x/slashing/spec/04_begin_block.md b/x/slashing/spec/04_begin_block.md index 18d6cbcc42f6..f99aa0d8a50f 100644 --- a/x/slashing/spec/04_begin_block.md +++ b/x/slashing/spec/04_begin_block.md @@ -1,93 +1,12 @@ # BeginBlock -## Evidence Handling - -Tendermint blocks can include -[Evidence](https://github.com/tendermint/tendermint/blob/master/docs/spec/blockchain/blockchain.md#evidence), which indicates that a validator committed malicious -behavior. The relevant information is forwarded to the application as ABCI Evidence -in `abci.RequestBeginBlock` so that the validator an be accordingly punished. - -For some `Evidence` submitted in `block` to be valid, it must satisfy: - -`Evidence.Timestamp >= block.Timestamp - MaxEvidenceAge` - -Where `Evidence.Timestamp` is the timestamp in the block at height -`Evidence.Height` and `block.Timestamp` is the current block timestamp. - -If valid evidence is included in a block, the validator's stake is reduced by -some penalty (`SlashFractionDoubleSign` for equivocation) of what their stake was -when the infraction occurred (rather than when the evidence was discovered). We -want to "follow the stake", i.e. the stake which contributed to the infraction -should be slashed, even if it has since been redelegated or started unbonding. - -We first need to loop through the unbondings and redelegations from the slashed -validator and track how much stake has since moved: - -```go -slashAmountUnbondings := 0 -slashAmountRedelegations := 0 - -unbondings := getUnbondings(validator.Address) -for unbond in unbondings { - - if was not bonded before evidence.Height or started unbonding before unbonding period ago { - continue - } - - burn := unbond.InitialTokens * SLASH_PROPORTION - slashAmountUnbondings += burn - - unbond.Tokens = max(0, unbond.Tokens - burn) -} - -// only care if source gets slashed because we're already bonded to destination -// so if destination validator gets slashed our delegation just has same shares -// of smaller pool. -redels := getRedelegationsBySource(validator.Address) -for redel in redels { - - if was not bonded before evidence.Height or started redelegating before unbonding period ago { - continue - } - - burn := redel.InitialTokens * SLASH_PROPORTION - slashAmountRedelegations += burn - - amount := unbondFromValidator(redel.Destination, burn) - destroy(amount) -} -``` - -We then slash the validator and tombstone them: - -``` -curVal := validator -oldVal := loadValidator(evidence.Height, evidence.Address) - -slashAmount := SLASH_PROPORTION * oldVal.Shares -slashAmount -= slashAmountUnbondings -slashAmount -= slashAmountRedelegations - -curVal.Shares = max(0, curVal.Shares - slashAmount) - -signInfo = SigningInfo.Get(val.Address) -signInfo.JailedUntil = MAX_TIME -signInfo.Tombstoned = true -SigningInfo.Set(val.Address, signInfo) -``` - -This ensures that offending validators are punished the same amount whether they -act as a single validator with X stake or as N validators with collectively X -stake. The amount slashed for all double signature infractions committed within a -single slashing period is capped as described in [overview.md](overview.md) under Tombstone Caps. - ## Liveness Tracking At the beginning of each block, we update the `ValidatorSigningInfo` for each validator and check if they've crossed below the liveness threshold over a sliding window. This sliding window is defined by `SignedBlocksWindow` and the index in this window is determined by `IndexOffset` found in the validator's -`ValidatorSigningInfo`. For each block processed, the `IndexOffset` is incrimented +`ValidatorSigningInfo`. For each block processed, the `IndexOffset` is incremented regardless if the validator signed or not. Once the index is determined, the `MissedBlocksBitArray` and `MissedBlocksCounter` are updated accordingly. diff --git a/x/slashing/spec/06_events.md b/x/slashing/spec/06_events.md index 7891ebfe5a39..8d4e616c76cd 100644 --- a/x/slashing/spec/06_events.md +++ b/x/slashing/spec/06_events.md @@ -5,16 +5,16 @@ The slashing module emits the following events/tags: ## BeginBlocker | Type | Attribute Key | Attribute Value | -|-------|---------------|-----------------------------| +| ----- | ------------- | --------------------------- | | slash | address | {validatorConsensusAddress} | | slash | power | {validatorPower} | | slash | reason | {slashReason} | | slash | jailed [0] | {validatorConsensusAddress} | -- [0] Only included if the validator is jailed. +- [0] Only included if the validator is jailed. | Type | Attribute Key | Attribute Value | -|----------|---------------|-----------------------------| +| -------- | ------------- | --------------------------- | | liveness | address | {validatorConsensusAddress} | | liveness | missed_blocks | {missedBlocksCounter} | | liveness | height | {blockHeight} | @@ -24,7 +24,7 @@ The slashing module emits the following events/tags: ### MsgUnjail | Type | Attribute Key | Attribute Value | -|---------|---------------|-----------------| +| ------- | ------------- | --------------- | | message | module | slashing | | message | action | unjail | | message | sender | {senderAddress} | diff --git a/x/slashing/spec/08_params.md b/x/slashing/spec/08_params.md index b9dfca051628..c68188eb4b7c 100644 --- a/x/slashing/spec/08_params.md +++ b/x/slashing/spec/08_params.md @@ -3,8 +3,7 @@ The slashing module contains the following parameters: | Key | Type | Example | -|-------------------------|------------------|------------------------| -| MaxEvidenceAge | string (time ns) | "120000000000" | +| ----------------------- | ---------------- | ---------------------- | | SignedBlocksWindow | string (int64) | "100" | | MinSignedPerWindow | string (dec) | "0.500000000000000000" | | DowntimeJailDuration | string (time ns) | "600000000000" | diff --git a/x/slashing/spec/README.md b/x/slashing/spec/README.md index fbcb704fff6f..0f0111433c31 100644 --- a/x/slashing/spec/README.md +++ b/x/slashing/spec/README.md @@ -9,6 +9,7 @@ The slashing module enables Cosmos SDK-based blockchains to disincentivize any a by a protocol-recognized actor with value at stake by penalizing them ("slashing"). Penalties may include, but are not limited to: + - Burning some amount of their stake - Removing their ability to vote on future blocks for a period of time. @@ -35,4 +36,3 @@ This module will be used by the Cosmos Hub, the first hub in the Cosmos ecosyste 7. **[Staking Tombstone](07_tombstone.md)** - [Abstract](07_tombstone.md#abstract) 8. **[Parameters](08_params.md)** -