Merge PR #5299: Migrate Equivocation Handling to x/evidence

This commit is contained in:
Alexander Bezobchuk 2019-12-02 20:58:14 -05:00 committed by GitHub
parent 5be87e40ce
commit 7953b525d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 995 additions and 381 deletions

View File

@ -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

View File

@ -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

25
x/evidence/abci.go Normal file
View File

@ -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))
}
}
}

View File

@ -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,6 +24,7 @@ const (
EventTypeSubmitEvidence = types.EventTypeSubmitEvidence
AttributeValueCategory = types.AttributeValueCategory
AttributeKeyEvidenceHash = types.AttributeKeyEvidenceHash
DefaultMaxEvidenceAge = types.DefaultMaxEvidenceAge
)
var (
@ -38,6 +40,10 @@ var (
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
)

View File

@ -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:
$ <appcli> 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, &params); err != nil {
return fmt.Errorf("failed to unmarshal params: %w", err)
}
return cliCtx.PrintOutput(params)
},
}
}
// QueryEvidenceCmd returns the command handler for evidence querying. Evidence

View File

@ -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)
}
}

View File

@ -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),
}
}

View File

@ -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))

View File

@ -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))

View File

@ -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)
}

View File

@ -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())))
}

View File

@ -22,17 +22,27 @@ type Keeper struct {
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,
stakingKeeper: stakingKeeper,
slashingKeeper: slashingKeeper,
codespace: codespace,
}
}

View File

@ -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()

View File

@ -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, &params)
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, &params)
}

View File

@ -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))
}

View File

@ -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, &params)
@ -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, &params)

View File

@ -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))
}

View File

@ -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

View File

@ -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,
}
}

View File

@ -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)
})
}
}

View File

@ -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)
}
)

View File

@ -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
}

View File

@ -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())
}

View File

@ -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

View File

@ -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,
}
}

View File

@ -2,6 +2,7 @@ package types
// Querier routes for the evidence module
const (
QueryParameters = "parameters"
QueryEvidence = "evidence"
QueryAllEvidence = "all_evidence"
)

View File

@ -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.

View File

@ -0,0 +1,7 @@
# Parameters
The evidence module contains the following parameters:
| Key | Type | Example |
| -------------- | ---------------- | -------------- |
| MaxEvidenceAge | string (time ns) | "120000000000" |

View File

@ -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)

View File

@ -1,5 +1,15 @@
# Evidence Module Specification
## Table of Contents
<!-- TOC -->
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.
<!-- TOC -->
1. **[Concepts](01_concepts.md)**
2. **[State](02_state.md)**
3. **[Messages](03_messages.md)**
4. **[Events](04_events.md)**

View File

@ -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 {

View File

@ -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 {

View File

@ -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))
}
}
}

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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) {

View File

@ -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)
}

View File

@ -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())

View File

@ -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,
)
}

View File

@ -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)

View File

@ -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.

View File

@ -5,7 +5,7 @@ The slashing module emits the following events/tags:
## BeginBlocker
| Type | Attribute Key | Attribute Value |
|-------|---------------|-----------------------------|
| ----- | ------------- | --------------------------- |
| slash | address | {validatorConsensusAddress} |
| slash | power | {validatorPower} |
| slash | reason | {slashReason} |
@ -14,7 +14,7 @@ The slashing module emits the following events/tags:
- [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} |

View File

@ -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" |

View File

@ -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)**