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 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: logic has been implemented for v0.38 target version. Applications can migrate via:
`$ {appd} migrate v0.38 genesis.json`. `$ {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 ### 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 * `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 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`. `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 ### Client Breaking Changes

View File

@ -194,6 +194,7 @@ func NewSimApp(
// create evidence keeper with router // create evidence keeper with router
evidenceKeeper := evidence.NewKeeper( evidenceKeeper := evidence.NewKeeper(
app.cdc, keys[evidence.StoreKey], app.subspaces[evidence.ModuleName], evidence.DefaultCodespace, app.cdc, keys[evidence.StoreKey], app.subspaces[evidence.ModuleName], evidence.DefaultCodespace,
&app.StakingKeeper, app.SlashingKeeper,
) )
evidenceRouter := evidence.NewRouter() evidenceRouter := evidence.NewRouter()
// TODO: Register evidence routes. // TODO: Register evidence routes.
@ -237,7 +238,7 @@ func NewSimApp(
// During begin block slashing happens after distr.BeginBlocker so that // 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 // there is nothing left over in the validator fee pool, so as to keep the
// CanWithdrawInvariant invariant. // 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) app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName)
// NOTE: The genutils moodule must occur after staking so that pools are // 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 DefaultParamspace = types.DefaultParamspace
QueryEvidence = types.QueryEvidence QueryEvidence = types.QueryEvidence
QueryAllEvidence = types.QueryAllEvidence QueryAllEvidence = types.QueryAllEvidence
QueryParameters = types.QueryParameters
CodeNoEvidenceHandlerExists = types.CodeNoEvidenceHandlerExists CodeNoEvidenceHandlerExists = types.CodeNoEvidenceHandlerExists
CodeInvalidEvidence = types.CodeInvalidEvidence CodeInvalidEvidence = types.CodeInvalidEvidence
CodeNoEvidenceExists = types.CodeNoEvidenceExists CodeNoEvidenceExists = types.CodeNoEvidenceExists
@ -23,21 +24,26 @@ const (
EventTypeSubmitEvidence = types.EventTypeSubmitEvidence EventTypeSubmitEvidence = types.EventTypeSubmitEvidence
AttributeValueCategory = types.AttributeValueCategory AttributeValueCategory = types.AttributeValueCategory
AttributeKeyEvidenceHash = types.AttributeKeyEvidenceHash AttributeKeyEvidenceHash = types.AttributeKeyEvidenceHash
DefaultMaxEvidenceAge = types.DefaultMaxEvidenceAge
) )
var ( var (
NewKeeper = keeper.NewKeeper NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier NewQuerier = keeper.NewQuerier
NewMsgSubmitEvidence = types.NewMsgSubmitEvidence NewMsgSubmitEvidence = types.NewMsgSubmitEvidence
NewRouter = types.NewRouter NewRouter = types.NewRouter
NewQueryEvidenceParams = types.NewQueryEvidenceParams NewQueryEvidenceParams = types.NewQueryEvidenceParams
NewQueryAllEvidenceParams = types.NewQueryAllEvidenceParams NewQueryAllEvidenceParams = types.NewQueryAllEvidenceParams
RegisterCodec = types.RegisterCodec RegisterCodec = types.RegisterCodec
RegisterEvidenceTypeCodec = types.RegisterEvidenceTypeCodec RegisterEvidenceTypeCodec = types.RegisterEvidenceTypeCodec
ModuleCdc = types.ModuleCdc ModuleCdc = types.ModuleCdc
NewGenesisState = types.NewGenesisState NewGenesisState = types.NewGenesisState
DefaultGenesisState = types.DefaultGenesisState DefaultGenesisState = types.DefaultGenesisState
ConvertDuplicateVoteEvidence = types.ConvertDuplicateVoteEvidence
KeyMaxEvidenceAge = types.KeyMaxEvidenceAge
DoubleSignJailEndTime = types.DoubleSignJailEndTime
ParamKeyTable = types.ParamKeyTable
) )
type ( type (
@ -47,4 +53,5 @@ type (
MsgSubmitEvidence = types.MsgSubmitEvidence MsgSubmitEvidence = types.MsgSubmitEvidence
Handler = types.Handler Handler = types.Handler
Router = types.Router 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(flagPage, 1, "pagination page of evidence to to query for")
cmd.Flags().Int(flagLimit, 100, "pagination limit of evidence 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 // QueryEvidenceCmd returns the command handler for evidence querying. Evidence

View File

@ -22,6 +22,11 @@ func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
"/evidence", "/evidence",
queryAllEvidenceHandler(cliCtx), queryAllEvidenceHandler(cliCtx),
).Methods(MethodGet) ).Methods(MethodGet)
r.HandleFunc(
"/evidence/params",
queryParamsHandler(cliCtx),
).Methods(MethodGet)
} }
func queryEvidenceHandler(cliCtx context.CLIContext) http.HandlerFunc { func queryEvidenceHandler(cliCtx context.CLIContext) http.HandlerFunc {
@ -89,3 +94,22 @@ func queryAllEvidenceHandler(cliCtx context.CLIContext) http.HandlerFunc {
rest.PostProcessResponse(w, cliCtx, res) 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.SetEvidence(ctx, e)
} }
k.SetParams(ctx, gs.Params)
} }
// ExportGenesis returns the evidence module's exported genesis. // ExportGenesis returns the evidence module's exported genesis.
func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState { func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState {
return GenesisState{ return GenesisState{
Params: k.GetParams(ctx),
Evidence: k.GetAllEvidence(ctx), Evidence: k.GetAllEvidence(ctx),
} }
} }

View File

@ -33,7 +33,7 @@ func (suite *GenesisTestSuite) SetupTest() {
// recreate keeper in order to use custom testing types // recreate keeper in order to use custom testing types
evidenceKeeper := evidence.NewKeeper( evidenceKeeper := evidence.NewKeeper(
cdc, app.GetKey(evidence.StoreKey), app.GetSubspace(evidence.ModuleName), cdc, app.GetKey(evidence.StoreKey), app.GetSubspace(evidence.ModuleName),
evidence.DefaultCodespace, evidence.DefaultCodespace, app.StakingKeeper, app.SlashingKeeper,
) )
router := evidence.NewRouter() router := evidence.NewRouter()
router = router.AddRoute(types.TestEvidenceRouteEquivocation, types.TestEquivocationHandler(*evidenceKeeper)) router = router.AddRoute(types.TestEvidenceRouteEquivocation, types.TestEquivocationHandler(*evidenceKeeper))
@ -67,7 +67,7 @@ func (suite *GenesisTestSuite) TestInitGenesis_Valid() {
} }
suite.NotPanics(func() { 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 { for _, e := range testEvidence {
@ -100,7 +100,7 @@ func (suite *GenesisTestSuite) TestInitGenesis_Invalid() {
} }
suite.Panics(func() { 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)) 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 // recreate keeper in order to use custom testing types
evidenceKeeper := evidence.NewKeeper( evidenceKeeper := evidence.NewKeeper(
cdc, app.GetKey(evidence.StoreKey), app.GetSubspace(evidence.ModuleName), cdc, app.GetKey(evidence.StoreKey), app.GetSubspace(evidence.ModuleName),
evidence.DefaultCodespace, evidence.DefaultCodespace, app.StakingKeeper, app.SlashingKeeper,
) )
router := evidence.NewRouter() router := evidence.NewRouter()
router = router.AddRoute(types.TestEvidenceRouteEquivocation, types.TestEquivocationHandler(*evidenceKeeper)) 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

@ -18,22 +18,32 @@ import (
// managing persistence, state transitions and query handling for the evidence // managing persistence, state transitions and query handling for the evidence
// module. // module.
type Keeper struct { type Keeper struct {
cdc *codec.Codec cdc *codec.Codec
storeKey sdk.StoreKey storeKey sdk.StoreKey
paramSpace params.Subspace paramSpace params.Subspace
router types.Router router types.Router
codespace sdk.CodespaceType stakingKeeper types.StakingKeeper
slashingKeeper types.SlashingKeeper
codespace sdk.CodespaceType
} }
func NewKeeper( func NewKeeper(
cdc *codec.Codec, storeKey sdk.StoreKey, paramSpace params.Subspace, codespace sdk.CodespaceType, cdc *codec.Codec, storeKey sdk.StoreKey, paramSpace params.Subspace, codespace sdk.CodespaceType,
stakingKeeper types.StakingKeeper, slashingKeeper types.SlashingKeeper,
) *Keeper { ) *Keeper {
// set KeyTable if it has not already been set
if !paramSpace.HasKeyTable() {
paramSpace = paramSpace.WithKeyTable(types.ParamKeyTable())
}
return &Keeper{ return &Keeper{
cdc: cdc, cdc: cdc,
storeKey: storeKey, storeKey: storeKey,
paramSpace: paramSpace, paramSpace: paramSpace,
codespace: codespace, stakingKeeper: stakingKeeper,
slashingKeeper: slashingKeeper,
codespace: codespace,
} }
} }

View File

@ -1,6 +1,7 @@
package keeper_test package keeper_test
import ( import (
"encoding/hex"
"testing" "testing"
"github.com/cosmos/cosmos-sdk/simapp" "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/exported"
"github.com/cosmos/cosmos-sdk/x/evidence/internal/keeper" "github.com/cosmos/cosmos-sdk/x/evidence/internal/keeper"
"github.com/cosmos/cosmos-sdk/x/evidence/internal/types" "github.com/cosmos/cosmos-sdk/x/evidence/internal/types"
"github.com/cosmos/cosmos-sdk/x/supply"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types" abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/ed25519" "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 { type KeeperTestSuite struct {
suite.Suite suite.Suite
ctx sdk.Context ctx sdk.Context
querier sdk.Querier querier sdk.Querier
keeper keeper.Keeper keeper keeper.Keeper
app *simapp.SimApp
} }
func (suite *KeeperTestSuite) SetupTest() { func (suite *KeeperTestSuite) SetupTest() {
@ -34,7 +67,7 @@ func (suite *KeeperTestSuite) SetupTest() {
// recreate keeper in order to use custom testing types // recreate keeper in order to use custom testing types
evidenceKeeper := evidence.NewKeeper( evidenceKeeper := evidence.NewKeeper(
cdc, app.GetKey(evidence.StoreKey), app.GetSubspace(evidence.ModuleName), cdc, app.GetKey(evidence.StoreKey), app.GetSubspace(evidence.ModuleName),
evidence.DefaultCodespace, evidence.DefaultCodespace, app.StakingKeeper, app.SlashingKeeper,
) )
router := evidence.NewRouter() router := evidence.NewRouter()
router = router.AddRoute(types.TestEvidenceRouteEquivocation, types.TestEquivocationHandler(*evidenceKeeper)) 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.ctx = app.BaseApp.NewContext(checkTx, abci.Header{Height: 1})
suite.querier = keeper.NewQuerier(*evidenceKeeper) suite.querier = keeper.NewQuerier(*evidenceKeeper)
suite.keeper = *evidenceKeeper suite.keeper = *evidenceKeeper
suite.app = app
} }
func (suite *KeeperTestSuite) populateEvidence(ctx sdk.Context, numEvidence int) []exported.Evidence { 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 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() { func (suite *KeeperTestSuite) TestSubmitValidEvidence() {
ctx := suite.ctx.WithIsCheckTx(false) ctx := suite.ctx.WithIsCheckTx(false)
pk := ed25519.GenPrivKey() 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] { switch path[0] {
case types.QueryParameters:
res, err = queryParams(ctx, k)
case types.QueryEvidence: case types.QueryEvidence:
res, err = queryEvidence(ctx, path[1:], req, k) res, err = queryEvidence(ctx, req, k)
case types.QueryAllEvidence: case types.QueryAllEvidence:
res, err = queryAllEvidence(ctx, path[1:], req, k) res, err = queryAllEvidence(ctx, req, k)
default: default:
err = sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName) 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 var params types.QueryEvidenceParams
err := k.cdc.UnmarshalJSON(req.Data, &params) 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 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 var params types.QueryAllEvidenceParams
err := k.cdc.UnmarshalJSON(req.Data, &params) 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.Nil(types.TestingCdc.UnmarshalJSON(bz, &e))
suite.Len(e, 0) 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) { func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterInterface((*exported.Evidence)(nil), nil) cdc.RegisterInterface((*exported.Evidence)(nil), nil)
cdc.RegisterConcrete(MsgSubmitEvidence{}, "cosmos-sdk/MsgSubmitEvidence", nil) cdc.RegisterConcrete(MsgSubmitEvidence{}, "cosmos-sdk/MsgSubmitEvidence", nil)
cdc.RegisterConcrete(Equivocation{}, "cosmos-sdk/Equivocation", nil)
} }
// RegisterEvidenceTypeCodec registers an external concrete Evidence type defined // 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 package types
import "github.com/cosmos/cosmos-sdk/x/evidence/exported" import (
"fmt"
"time"
"github.com/cosmos/cosmos-sdk/x/evidence/exported"
)
// DONTCOVER // DONTCOVER
// GenesisState defines the evidence module's genesis state. // GenesisState defines the evidence module's genesis state.
type GenesisState struct { type GenesisState struct {
Params Params `json:"params" yaml:"params"`
Evidence []exported.Evidence `json:"evidence" yaml:"evidence"` Evidence []exported.Evidence `json:"evidence" yaml:"evidence"`
} }
func NewGenesisState(e []exported.Evidence) GenesisState { func NewGenesisState(p Params, e []exported.Evidence) GenesisState {
return GenesisState{Evidence: e} return GenesisState{
Params: p,
Evidence: e,
}
} }
// DefaultGenesisState returns the evidence module's default genesis state. // DefaultGenesisState returns the evidence module's default genesis state.
func DefaultGenesisState() GenesisState { 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 // 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 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()) 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()) require.Error(t, gs.Validate())
} }

View File

@ -12,9 +12,6 @@ const (
// QuerierRoute defines the module's query routing key // QuerierRoute defines the module's query routing key
QuerierRoute = ModuleName QuerierRoute = ModuleName
// DefaultParamspace defines the module's default paramspace name
DefaultParamspace = ModuleName
) )
// KVStore key prefixes // 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 // Querier routes for the evidence module
const ( const (
QueryParameters = "parameters"
QueryEvidence = "evidence" QueryEvidence = "evidence"
QueryAllEvidence = "all_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. // 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 // EndBlock executes all ABCI EndBlock logic respective to the evidence module. It
// returns no validator updates. // 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 # 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 ## Abstract
`x/evidence` is an implementation of a Cosmos SDK module, per [ADR 009](./../../../docs/architecture/adr-009-evidence-module.md), `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 Each corresponding handler must also fulfill the `Handler` interface contract. The
`Handler` for a given `Evidence` type can perform any arbitrary state transitions `Handler` for a given `Evidence` type can perform any arbitrary state transitions
such as slashing, jailing, and tombstoning. 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() cdc, ctx, skey, _, keeper := testComponents()
store := prefix.NewStore(ctx.KVStore(skey), []byte("test/")) 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 // Set params
for i, kv := range kvs { 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 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 // WithKeyTable initializes KeyTable and returns modified Subspace
func (s Subspace) WithKeyTable(table KeyTable) Subspace { func (s Subspace) WithKeyTable(table KeyTable) Subspace {
if table.m == nil { if table.m == nil {

View File

@ -1,10 +1,7 @@
package slashing package slashing
import ( import (
"fmt"
abci "github.com/tendermint/tendermint/abci/types" abci "github.com/tendermint/tendermint/abci/types"
tmtypes "github.com/tendermint/tendermint/types"
sdk "github.com/cosmos/cosmos-sdk/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() { for _, voteInfo := range req.LastCommitInfo.GetVotes() {
k.HandleValidatorSignature(ctx, voteInfo.Validator.Address, voteInfo.Validator.Power, voteInfo.SignedLastBlock) 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 RouterKey = types.RouterKey
QuerierRoute = types.QuerierRoute QuerierRoute = types.QuerierRoute
DefaultParamspace = types.DefaultParamspace DefaultParamspace = types.DefaultParamspace
DefaultMaxEvidenceAge = types.DefaultMaxEvidenceAge
DefaultSignedBlocksWindow = types.DefaultSignedBlocksWindow DefaultSignedBlocksWindow = types.DefaultSignedBlocksWindow
DefaultDowntimeJailDuration = types.DefaultDowntimeJailDuration DefaultDowntimeJailDuration = types.DefaultDowntimeJailDuration
QueryParameters = types.QueryParameters QueryParameters = types.QueryParameters
@ -77,11 +76,9 @@ var (
ValidatorSigningInfoKey = types.ValidatorSigningInfoKey ValidatorSigningInfoKey = types.ValidatorSigningInfoKey
ValidatorMissedBlockBitArrayKey = types.ValidatorMissedBlockBitArrayKey ValidatorMissedBlockBitArrayKey = types.ValidatorMissedBlockBitArrayKey
AddrPubkeyRelationKey = types.AddrPubkeyRelationKey AddrPubkeyRelationKey = types.AddrPubkeyRelationKey
DoubleSignJailEndTime = types.DoubleSignJailEndTime
DefaultMinSignedPerWindow = types.DefaultMinSignedPerWindow DefaultMinSignedPerWindow = types.DefaultMinSignedPerWindow
DefaultSlashFractionDoubleSign = types.DefaultSlashFractionDoubleSign DefaultSlashFractionDoubleSign = types.DefaultSlashFractionDoubleSign
DefaultSlashFractionDowntime = types.DefaultSlashFractionDowntime DefaultSlashFractionDowntime = types.DefaultSlashFractionDowntime
KeyMaxEvidenceAge = types.KeyMaxEvidenceAge
KeySignedBlocksWindow = types.KeySignedBlocksWindow KeySignedBlocksWindow = types.KeySignedBlocksWindow
KeyMinSignedPerWindow = types.KeyMinSignedPerWindow KeyMinSignedPerWindow = types.KeyMinSignedPerWindow
KeyDowntimeJailDuration = types.KeyDowntimeJailDuration KeyDowntimeJailDuration = types.KeyDowntimeJailDuration

View File

@ -2,7 +2,6 @@ package keeper
import ( import (
"fmt" "fmt"
"time"
"github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto"
@ -10,107 +9,6 @@ import (
"github.com/cosmos/cosmos-sdk/x/slashing/internal/types" "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. // 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) { func (k Keeper) HandleValidatorSignature(ctx sdk.Context, addr crypto.Address, power int64, signed bool) {
logger := k.Logger(ctx) 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 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) { func (k Keeper) setAddrPubkeyRelation(ctx sdk.Context, addr crypto.Address, pubkey crypto.PubKey) {
store := ctx.KVStore(k.storeKey) store := ctx.KVStore(k.storeKey)
bz := k.cdc.MustMarshalBinaryLengthPrefixed(pubkey) bz := k.cdc.MustMarshalBinaryLengthPrefixed(pubkey)

View File

@ -5,111 +5,12 @@ import (
"time" "time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/slashing/internal/types" "github.com/cosmos/cosmos-sdk/x/slashing/internal/types"
"github.com/cosmos/cosmos-sdk/x/staking" "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 // Test a new validator entering the validator set
// Ensure that SigningInfo.StartHeight is set correctly // Ensure that SigningInfo.StartHeight is set correctly
// and that they are not immediately jailed // and that they are not immediately jailed

View File

@ -7,12 +7,6 @@ import (
"github.com/cosmos/cosmos-sdk/x/slashing/internal/types" "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 // SignedBlocksWindow - sliding window for downtime slashing
func (k Keeper) SignedBlocksWindow(ctx sdk.Context) (res int64) { func (k Keeper) SignedBlocksWindow(ctx sdk.Context) (res int64) {
k.paramspace.Get(ctx, types.KeySignedBlocksWindow, &res) k.paramspace.Get(ctx, types.KeySignedBlocksWindow, &res)

View File

@ -1,6 +1,8 @@
package keeper package keeper
import ( import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/slashing/internal/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 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 // SetValidatorSigningInfo sets the validator signing info to a consensus address key
func (k Keeper) SetValidatorSigningInfo(ctx sdk.Context, address sdk.ConsAddress, info types.ValidatorSigningInfo) { func (k Keeper) SetValidatorSigningInfo(ctx sdk.Context, address sdk.ConsAddress, info types.ValidatorSigningInfo) {
store := ctx.KVStore(k.storeKey) 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 // SetValidatorMissedBlockBitArray sets the bit that checks if the validator has
// missed a block in the current window // missed a block in the current window
func (k Keeper) SetValidatorMissedBlockBitArray(ctx sdk.Context, address sdk.ConsAddress, index int64, missed bool) { 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) missed = keeper.GetValidatorMissedBlockBitArray(ctx, sdk.ConsAddress(Addrs[0]), 0)
require.True(t, missed) // now should be missed 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()) 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 downtimeJail := data.Params.DowntimeJailDuration
if downtimeJail < 1*time.Minute { if downtimeJail < 1*time.Minute {
return fmt.Errorf("downtime unblond duration must be at least 1 minute, is %s", downtimeJail.String()) 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 // Default parameter namespace
const ( const (
DefaultParamspace = ModuleName DefaultParamspace = ModuleName
DefaultMaxEvidenceAge = 60 * 2 * time.Second
DefaultSignedBlocksWindow = int64(100) DefaultSignedBlocksWindow = int64(100)
DefaultDowntimeJailDuration = 60 * 10 * time.Second 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 ( var (
DoubleSignJailEndTime = time.Unix(253402300799, 0)
DefaultMinSignedPerWindow = sdk.NewDecWithPrec(5, 1) DefaultMinSignedPerWindow = sdk.NewDecWithPrec(5, 1)
DefaultSlashFractionDoubleSign = sdk.NewDec(1).Quo(sdk.NewDec(20)) DefaultSlashFractionDoubleSign = sdk.NewDec(1).Quo(sdk.NewDec(20))
DefaultSlashFractionDowntime = sdk.NewDec(1).Quo(sdk.NewDec(100)) DefaultSlashFractionDowntime = sdk.NewDec(1).Quo(sdk.NewDec(100))
@ -26,7 +23,6 @@ var (
// Parameter store keys // Parameter store keys
var ( var (
KeyMaxEvidenceAge = []byte("MaxEvidenceAge")
KeySignedBlocksWindow = []byte("SignedBlocksWindow") KeySignedBlocksWindow = []byte("SignedBlocksWindow")
KeyMinSignedPerWindow = []byte("MinSignedPerWindow") KeyMinSignedPerWindow = []byte("MinSignedPerWindow")
KeyDowntimeJailDuration = []byte("DowntimeJailDuration") KeyDowntimeJailDuration = []byte("DowntimeJailDuration")
@ -41,7 +37,6 @@ func ParamKeyTable() params.KeyTable {
// Params - used for initializing default parameter for slashing at genesis // Params - used for initializing default parameter for slashing at genesis
type Params struct { type Params struct {
MaxEvidenceAge time.Duration `json:"max_evidence_age" yaml:"max_evidence_age"`
SignedBlocksWindow int64 `json:"signed_blocks_window" yaml:"signed_blocks_window"` SignedBlocksWindow int64 `json:"signed_blocks_window" yaml:"signed_blocks_window"`
MinSignedPerWindow sdk.Dec `json:"min_signed_per_window" yaml:"min_signed_per_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"` DowntimeJailDuration time.Duration `json:"downtime_jail_duration" yaml:"downtime_jail_duration"`
@ -50,12 +45,12 @@ type Params struct {
} }
// NewParams creates a new Params object // NewParams creates a new Params object
func NewParams(maxEvidenceAge time.Duration, signedBlocksWindow int64, func NewParams(
minSignedPerWindow sdk.Dec, downtimeJailDuration time.Duration, signedBlocksWindow int64, minSignedPerWindow sdk.Dec, downtimeJailDuration time.Duration,
slashFractionDoubleSign, slashFractionDowntime sdk.Dec) Params { slashFractionDoubleSign, slashFractionDowntime sdk.Dec,
) Params {
return Params{ return Params{
MaxEvidenceAge: maxEvidenceAge,
SignedBlocksWindow: signedBlocksWindow, SignedBlocksWindow: signedBlocksWindow,
MinSignedPerWindow: minSignedPerWindow, MinSignedPerWindow: minSignedPerWindow,
DowntimeJailDuration: downtimeJailDuration, DowntimeJailDuration: downtimeJailDuration,
@ -67,12 +62,11 @@ func NewParams(maxEvidenceAge time.Duration, signedBlocksWindow int64,
// String implements the stringer interface for Params // String implements the stringer interface for Params
func (p Params) String() string { func (p Params) String() string {
return fmt.Sprintf(`Slashing Params: return fmt.Sprintf(`Slashing Params:
MaxEvidenceAge: %s
SignedBlocksWindow: %d SignedBlocksWindow: %d
MinSignedPerWindow: %s MinSignedPerWindow: %s
DowntimeJailDuration: %s DowntimeJailDuration: %s
SlashFractionDoubleSign: %s SlashFractionDoubleSign: %s
SlashFractionDowntime: %s`, p.MaxEvidenceAge, SlashFractionDowntime: %s`,
p.SignedBlocksWindow, p.MinSignedPerWindow, p.SignedBlocksWindow, p.MinSignedPerWindow,
p.DowntimeJailDuration, p.SlashFractionDoubleSign, p.DowntimeJailDuration, p.SlashFractionDoubleSign,
p.SlashFractionDowntime) p.SlashFractionDowntime)
@ -81,7 +75,6 @@ func (p Params) String() string {
// ParamSetPairs - Implements params.ParamSet // ParamSetPairs - Implements params.ParamSet
func (p *Params) ParamSetPairs() params.ParamSetPairs { func (p *Params) ParamSetPairs() params.ParamSetPairs {
return params.ParamSetPairs{ return params.ParamSetPairs{
params.NewParamSetPair(KeyMaxEvidenceAge, &p.MaxEvidenceAge),
params.NewParamSetPair(KeySignedBlocksWindow, &p.SignedBlocksWindow), params.NewParamSetPair(KeySignedBlocksWindow, &p.SignedBlocksWindow),
params.NewParamSetPair(KeyMinSignedPerWindow, &p.MinSignedPerWindow), params.NewParamSetPair(KeyMinSignedPerWindow, &p.MinSignedPerWindow),
params.NewParamSetPair(KeyDowntimeJailDuration, &p.DowntimeJailDuration), params.NewParamSetPair(KeyDowntimeJailDuration, &p.DowntimeJailDuration),
@ -93,7 +86,7 @@ func (p *Params) ParamSetPairs() params.ParamSetPairs {
// DefaultParams defines the parameters for this module // DefaultParams defines the parameters for this module
func DefaultParams() Params { func DefaultParams() Params {
return NewParams( return NewParams(
DefaultMaxEvidenceAge, DefaultSignedBlocksWindow, DefaultMinSignedPerWindow, DefaultSignedBlocksWindow, DefaultMinSignedPerWindow, DefaultDowntimeJailDuration,
DefaultDowntimeJailDuration, DefaultSlashFractionDoubleSign, DefaultSlashFractionDowntime, DefaultSlashFractionDoubleSign, DefaultSlashFractionDowntime,
) )
} }

View File

@ -82,8 +82,8 @@ func RandomizedGenState(simState *module.SimulationState) {
) )
params := types.NewParams( params := types.NewParams(
simState.UnbondTime, signedBlocksWindow, minSignedPerWindow, signedBlocksWindow, minSignedPerWindow, downtimeJailDuration,
downtimeJailDuration, slashFractionDoubleSign, slashFractionDowntime, slashFractionDoubleSign, slashFractionDowntime,
) )
slashingGenesis := types.NewGenesisState(params, nil, nil) slashingGenesis := types.NewGenesisState(params, nil, nil)

View File

@ -1,93 +1,12 @@
# BeginBlock # 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 ## Liveness Tracking
At the beginning of each block, we update the `ValidatorSigningInfo` for each 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 validator and check if they've crossed below the liveness threshold over a
sliding window. This sliding window is defined by `SignedBlocksWindow` and the sliding window. This sliding window is defined by `SignedBlocksWindow` and the
index in this window is determined by `IndexOffset` found in the validator's 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 regardless if the validator signed or not. Once the index is determined, the
`MissedBlocksBitArray` and `MissedBlocksCounter` are updated accordingly. `MissedBlocksBitArray` and `MissedBlocksCounter` are updated accordingly.

View File

@ -5,16 +5,16 @@ The slashing module emits the following events/tags:
## BeginBlocker ## BeginBlocker
| Type | Attribute Key | Attribute Value | | Type | Attribute Key | Attribute Value |
|-------|---------------|-----------------------------| | ----- | ------------- | --------------------------- |
| slash | address | {validatorConsensusAddress} | | slash | address | {validatorConsensusAddress} |
| slash | power | {validatorPower} | | slash | power | {validatorPower} |
| slash | reason | {slashReason} | | slash | reason | {slashReason} |
| slash | jailed [0] | {validatorConsensusAddress} | | 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 | | Type | Attribute Key | Attribute Value |
|----------|---------------|-----------------------------| | -------- | ------------- | --------------------------- |
| liveness | address | {validatorConsensusAddress} | | liveness | address | {validatorConsensusAddress} |
| liveness | missed_blocks | {missedBlocksCounter} | | liveness | missed_blocks | {missedBlocksCounter} |
| liveness | height | {blockHeight} | | liveness | height | {blockHeight} |
@ -24,7 +24,7 @@ The slashing module emits the following events/tags:
### MsgUnjail ### MsgUnjail
| Type | Attribute Key | Attribute Value | | Type | Attribute Key | Attribute Value |
|---------|---------------|-----------------| | ------- | ------------- | --------------- |
| message | module | slashing | | message | module | slashing |
| message | action | unjail | | message | action | unjail |
| message | sender | {senderAddress} | | message | sender | {senderAddress} |

View File

@ -3,8 +3,7 @@
The slashing module contains the following parameters: The slashing module contains the following parameters:
| Key | Type | Example | | Key | Type | Example |
|-------------------------|------------------|------------------------| | ----------------------- | ---------------- | ---------------------- |
| MaxEvidenceAge | string (time ns) | "120000000000" |
| SignedBlocksWindow | string (int64) | "100" | | SignedBlocksWindow | string (int64) | "100" |
| MinSignedPerWindow | string (dec) | "0.500000000000000000" | | MinSignedPerWindow | string (dec) | "0.500000000000000000" |
| DowntimeJailDuration | string (time ns) | "600000000000" | | 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"). by a protocol-recognized actor with value at stake by penalizing them ("slashing").
Penalties may include, but are not limited to: Penalties may include, but are not limited to:
- Burning some amount of their stake - Burning some amount of their stake
- Removing their ability to vote on future blocks for a period of time. - 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)** 7. **[Staking Tombstone](07_tombstone.md)**
- [Abstract](07_tombstone.md#abstract) - [Abstract](07_tombstone.md#abstract)
8. **[Parameters](08_params.md)** 8. **[Parameters](08_params.md)**