Merge PR #5299: Migrate Equivocation Handling to x/evidence
This commit is contained in:
parent
5be87e40ce
commit
7953b525d8
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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, ¶ms); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal params: %w", err)
|
||||
}
|
||||
|
||||
return cliCtx.PrintOutput(params)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// QueryEvidenceCmd returns the command handler for evidence querying. Evidence
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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())))
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
package keeper
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/x/evidence/internal/types"
|
||||
)
|
||||
|
||||
// MaxEvidenceAge returns the maximum age for submitted evidence.
|
||||
func (k Keeper) MaxEvidenceAge(ctx sdk.Context) (res time.Duration) {
|
||||
k.paramSpace.Get(ctx, types.KeyMaxEvidenceAge, &res)
|
||||
return
|
||||
}
|
||||
|
||||
// GetParams returns the total set of evidence parameters.
|
||||
func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) {
|
||||
k.paramSpace.GetParamSet(ctx, ¶ms)
|
||||
return params
|
||||
}
|
||||
|
||||
// SetParams sets the evidence parameters to the param space.
|
||||
func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
|
||||
k.paramSpace.SetParamSet(ctx, ¶ms)
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -21,11 +21,14 @@ func NewQuerier(k Keeper) sdk.Querier {
|
|||
)
|
||||
|
||||
switch path[0] {
|
||||
case types.QueryParameters:
|
||||
res, err = queryParams(ctx, k)
|
||||
|
||||
case types.QueryEvidence:
|
||||
res, err = queryEvidence(ctx, path[1:], req, k)
|
||||
res, err = queryEvidence(ctx, req, k)
|
||||
|
||||
case types.QueryAllEvidence:
|
||||
res, err = queryAllEvidence(ctx, path[1:], req, k)
|
||||
res, err = queryAllEvidence(ctx, req, k)
|
||||
|
||||
default:
|
||||
err = sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
|
||||
|
@ -35,7 +38,18 @@ func NewQuerier(k Keeper) sdk.Querier {
|
|||
}
|
||||
}
|
||||
|
||||
func queryEvidence(ctx sdk.Context, _ []string, req abci.RequestQuery, k Keeper) ([]byte, error) {
|
||||
func queryParams(ctx sdk.Context, k Keeper) ([]byte, error) {
|
||||
params := k.GetParams(ctx)
|
||||
|
||||
res, err := codec.MarshalJSONIndent(k.cdc, params)
|
||||
if err != nil {
|
||||
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func queryEvidence(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
|
||||
var params types.QueryEvidenceParams
|
||||
|
||||
err := k.cdc.UnmarshalJSON(req.Data, ¶ms)
|
||||
|
@ -61,7 +75,7 @@ func queryEvidence(ctx sdk.Context, _ []string, req abci.RequestQuery, k Keeper)
|
|||
return res, nil
|
||||
}
|
||||
|
||||
func queryAllEvidence(ctx sdk.Context, _ []string, req abci.RequestQuery, k Keeper) ([]byte, error) {
|
||||
func queryAllEvidence(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
|
||||
var params types.QueryAllEvidenceParams
|
||||
|
||||
err := k.cdc.UnmarshalJSON(req.Data, ¶ms)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
)
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package types
|
|||
|
||||
// Querier routes for the evidence module
|
||||
const (
|
||||
QueryParameters = "parameters"
|
||||
QueryEvidence = "evidence"
|
||||
QueryAllEvidence = "all_evidence"
|
||||
)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# Parameters
|
||||
|
||||
The evidence module contains the following parameters:
|
||||
|
||||
| Key | Type | Example |
|
||||
| -------------- | ---------------- | -------------- |
|
||||
| MaxEvidenceAge | string (time ns) | "120000000000" |
|
|
@ -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)
|
|
@ -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)**
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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} |
|
||||
|
|
|
@ -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" |
|
||||
|
|
|
@ -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)**
|
||||
|
||||
|
|
Loading…
Reference in New Issue