Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Customizable Slashing and Jailing #2403

Merged
merged 15 commits into from
Dec 3, 2024
stana-miric marked this conversation as resolved.
Show resolved Hide resolved
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Allow consumer chains to customize the slashing and jailing conditions. Every consumer chain can decide the punishment for every type of infraction.
stana-miric marked this conversation as resolved.
Show resolved Hide resolved
([\#2403](https://github.com/cosmos/interchain-security/pull/2403))
10 changes: 7 additions & 3 deletions docs/docs/features/slashing.md
stana-miric marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ The ICS protocol differentiates between downtime and equivocation infractions.
## Downtime Infractions

Downtime infractions are reported by consumer chains and are acted upon on the provider as soon as they are received.
Instead of slashing, the provider will **_only jail_** offending validator for the duration of time established by the provider chain parameters.
Note that validators are only jailed for downtime on consumer chains that they opted in to validate on,
The provider will jail and slash the offending validator. The jailing duration and slashing fraction are determined by the consumer's downtime infraction parameters on the provider chain.
Note that validators are only slashed and jailed for downtime on consumer chains that they opted in to validate on,
or in the case of Top N chains, where they are automatically opted in by being in the Top N% of the validator set on the provider.
stana-miric marked this conversation as resolved.
Show resolved Hide resolved

For preventing malicious consumer chains from harming the provider, [slash throttling](../adrs/adr-002-throttle.md) (also known as _jail throttling_) ensures that only a fraction of the provider validator set can be jailed at any given time.
Expand All @@ -24,7 +24,7 @@ For preventing malicious consumer chains from harming the provider, [slash throt

Equivocation infractions are reported by external agents (e.g., relayers) that can submit to the provider evidence of light client or double signing attacks observed on a consumer chain.
The evidence is submitted by sending `MsgSubmitConsumerMisbehaviour` or `MsgSubmitConsumerDoubleVoting` messages to the provider.
When valid evidence is received, the malicious validators are slashed, jailed, and tombstoned on the provider.
When valid evidence is received, the malicious validators are slashed, jailed, and tombstoned on the provider. The jailing duration and slashing fraction are determined by the consumer's double sign infraction parameters on the provider chain.
This is enabled through the _cryptographic verification of equivocation_ feature.
For more details, see [ADR-005](../adrs/adr-005-cryptographic-equivocation-verification.md) and [ADR-013](../adrs/adr-013-equivocation-slashing.md).

Expand Down Expand Up @@ -597,3 +597,7 @@ The following command demonstrates how to run a Hermes instance in _evidence mod
hermes evidence --chain <CONSUMER-CHAIN-ID>
```
Note that `hermes evidence` takes a `--check-past-blocks` option giving the possibility to look for older evidence (default is 100).

### Infraction parameters

Jailing and slashing for misbehavior on a consumer chain are governed by parameters defined on the provider chain for that specific consumer chain. To create or update these infraction parameters, use the MsgCreateConsumer or MsgUpdateConsumer messages. When creating a consumer chain, if custom infraction parameters are not specified, default values from the provider are applied. For updates, parameters can be modified immediately if the chain is in the pre-launch phase. If the chain has already launched, the update will be scheduled to take effect after the unbonding period expires. This ensures that changes are applied seamlessly based on the chain's lifecycle.
19 changes: 19 additions & 0 deletions proto/interchain_security/ccv/provider/v1/provider.proto
Original file line number Diff line number Diff line change
Expand Up @@ -556,3 +556,22 @@ enum ConsumerPhase {
message AllowlistedRewardDenoms {
repeated string denoms = 1;
}

//
message InfractionParameters {
SlashJailParameters double_sign = 1;
SlashJailParameters downtime = 2;
}

//
message SlashJailParameters {
bytes slash_fraction = 1 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false,
(amino.dont_omitempty) = true
];
// for permanent jailing use 9223372036854775807 which is the largest value a time.Duration can hold (approximately 292 years)
google.protobuf.Duration jail_duration = 8
stana-miric marked this conversation as resolved.
Show resolved Hide resolved
[ (gogoproto.nullable) = false, (gogoproto.stdduration) = true ];
}
3 changes: 3 additions & 0 deletions proto/interchain_security/ccv/provider/v1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ message Chain {
// filled with these validators first, and other validators will be added to the validator set only if there are
// not enough eligible priority validators.
repeated string prioritylist = 15;
// Infraction parameters for slashing and jailing
InfractionParameters infraction_parameters = 16;
}

message QueryValidatorConsumerAddrRequest {
Expand Down Expand Up @@ -397,6 +399,7 @@ message QueryConsumerChainResponse {
ConsumerMetadata metadata = 5 [ (gogoproto.nullable) = false ];
ConsumerInitializationParameters init_params = 6;
PowerShapingParameters power_shaping_params = 7;
InfractionParameters infraction_parameters = 8;
}

message QueryConsumerGenesisTimeRequest {
Expand Down
6 changes: 6 additions & 0 deletions proto/interchain_security/ccv/provider/v1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,9 @@ message MsgCreateConsumer {

// allowlisted reward denoms of the consumer
AllowlistedRewardDenoms allowlisted_reward_denoms = 6;

// infraction parameters for slashing and jailing
InfractionParameters infraction_parameters = 7;
stana-miric marked this conversation as resolved.
Show resolved Hide resolved
}

// MsgCreateConsumerResponse defines response type for MsgCreateConsumer
Expand Down Expand Up @@ -399,6 +402,9 @@ message MsgUpdateConsumer {
// the chain id CANNOT be updated.
// This field is optional and can remain empty (i.e., `new_chain_id = ""`) or correspond to the chain id the chain already has.
string new_chain_id = 8;

// infraction parameters for slashing and jailing
InfractionParameters infraction_parameters = 9;
}

// MsgUpdateConsumerResponse defines response type for MsgUpdateConsumer messages
Expand Down
7 changes: 4 additions & 3 deletions tests/integration/double_vote.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,10 +257,10 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() {

// verifies that the val gets slashed and has fewer tokens after the slashing
val, _ := s.providerApp.GetTestStakingKeeper().GetValidator(provCtx, provAddr.ToSdkConsAddr().Bytes())
slashFraction, err := s.providerApp.GetTestSlashingKeeper().SlashFractionDoubleSign(provCtx)
infractionParam, err := s.providerApp.GetProviderKeeper().GetInfractionParameters(provCtx, tc.consumerId)
s.Require().NoError(err)
actualTokens := math.LegacyNewDecFromInt(val.GetTokens())
s.Require().True(initialTokens.Sub(initialTokens.Mul(slashFraction)).Equal(actualTokens))
s.Require().True(initialTokens.Sub(initialTokens.Mul(infractionParam.DoubleSign.SlashFraction)).Equal(actualTokens))
} else {
s.Require().Error(err)

Expand Down Expand Up @@ -409,8 +409,9 @@ func (s *CCVTestSuite) TestHandleConsumerDoubleVotingSlashesUndelegationsAndRele
)
s.Require().NoError(err)

slashFraction, err := s.providerApp.GetTestSlashingKeeper().SlashFractionDoubleSign(s.providerCtx())
infractionParam, err := s.providerApp.GetProviderKeeper().GetInfractionParameters(s.providerCtx(), s.getFirstBundle().ConsumerId)
s.Require().NoError(err)
slashFraction := infractionParam.DoubleSign.SlashFraction

// check undelegations are slashed
ubds, _ = s.providerApp.GetTestStakingKeeper().GetUnbondingDelegation(s.providerCtx(), delAddr, valAddr)
Expand Down
3 changes: 2 additions & 1 deletion tests/integration/misbehaviour.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,9 @@ func (s *CCVTestSuite) TestHandleConsumerMisbehaviour() {
s.Require().True(s.providerApp.GetTestSlashingKeeper().IsTombstoned(s.providerCtx(), provAddr.ToSdkConsAddr()))

validator, _ := s.providerApp.GetTestStakingKeeper().GetValidator(s.providerCtx(), provAddr.ToSdkConsAddr().Bytes())
slashFraction, err := s.providerApp.GetTestSlashingKeeper().SlashFractionDoubleSign(s.providerCtx())
infractionParam, err := s.providerApp.GetProviderKeeper().GetInfractionParameters(s.providerCtx(), s.getFirstBundle().ConsumerId)
s.Require().NoError(err)
slashFraction := infractionParam.DoubleSign.SlashFraction
actualTokens := math.LegacyNewDecFromInt(validator.GetTokens())
s.Require().True(initialTokens.Sub(initialTokens.Mul(slashFraction)).Equal(actualTokens))
}
Expand Down
4 changes: 4 additions & 0 deletions testutil/ibc_testing/generic_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ func AddConsumer[Tp testutil.ProviderApp, Tc testutil.ConsumerApp](
powerShapingParameters := testkeeper.GetTestPowerShapingParameters()
powerShapingParameters.Top_N = consumerTopNParams[index] // isn't used in CreateConsumerClient

infractionPrameters := testkeeper.GetTestInfractionParameters()

consumerId := providerKeeper.FetchAndIncrementConsumerId(providerChain.GetContext())
providerKeeper.SetConsumerChainId(providerChain.GetContext(), consumerId, chainID)
err := providerKeeper.SetConsumerMetadata(providerChain.GetContext(), consumerId, consumerMetadata)
Expand All @@ -164,6 +166,8 @@ func AddConsumer[Tp testutil.ProviderApp, Tc testutil.ConsumerApp](
s.Require().NoError(err)
err = providerKeeper.SetConsumerPowerShapingParameters(providerChain.GetContext(), consumerId, powerShapingParameters)
s.Require().NoError(err)
err = providerKeeper.SetInfractionParameters(providerChain.GetContext(), consumerId, infractionPrameters)
s.Require().NoError(err)
providerKeeper.SetConsumerPhase(providerChain.GetContext(), consumerId, providertypes.CONSUMER_PHASE_INITIALIZED)
if chainID == firstConsumerChainID {
FirstConsumerID = consumerId
Expand Down
5 changes: 4 additions & 1 deletion testutil/keeper/expectations.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,19 @@ func GetMocksForHandleSlashPacket(ctx sdk.Context, mocks MockedKeepers,

mocks.MockSlashingKeeper.EXPECT().IsTombstoned(ctx,
expectedProviderValConsAddr.ToSdkConsAddr()).Return(false).Times(1),

mocks.MockStakingKeeper.EXPECT().SlashWithInfractionReason(ctx, expectedProviderValConsAddr.ToSdkConsAddr(), gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any()).Return(math.NewInt(0), nil).Times(1),
}

if expectJailing {
// jail
calls = append(calls, mocks.MockStakingKeeper.EXPECT().Jail(
gomock.Eq(ctx),
gomock.Eq(expectedProviderValConsAddr.ToSdkConsAddr()),
).Return(nil))

// JailUntil is set in this code path.
calls = append(calls, mocks.MockSlashingKeeper.EXPECT().DowntimeJailDuration(ctx).Return(time.Hour, nil).Times(1))
calls = append(calls, mocks.MockSlashingKeeper.EXPECT().JailUntil(ctx,
expectedProviderValConsAddr.ToSdkConsAddr(), gomock.Any()).Return(nil).Times(1))
}
Expand Down
14 changes: 14 additions & 0 deletions testutil/keeper/unit_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/require"

"cosmossdk.io/log"
math "cosmossdk.io/math"
"cosmossdk.io/store"
"cosmossdk.io/store/metrics"
storetypes "cosmossdk.io/store/types"
Expand Down Expand Up @@ -300,6 +301,19 @@ func GetTestInitializationParameters() providertypes.ConsumerInitializationParam
}
}

func GetTestInfractionParameters() providertypes.InfractionParameters {
return providertypes.InfractionParameters{
DoubleSign: &providertypes.SlashJailParameters{
JailDuration: 1200 * time.Second,
SlashFraction: math.LegacyNewDecWithPrec(5, 1), // 0.5
},
Downtime: &providertypes.SlashJailParameters{
JailDuration: 600 * time.Second,
SlashFraction: math.LegacyNewDec(0),
},
}
}

func GetTestPowerShapingParameters() providertypes.PowerShapingParameters {
return providertypes.PowerShapingParameters{
Top_N: 0,
Expand Down
24 changes: 22 additions & 2 deletions x/ccv/provider/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,16 @@ where create_consumer.json has the following structure:
"allow_inactive_vals": false,
"prioritylist": ["cosmosvalcons..."]
},
"infraction_parameters": {
stana-miric marked this conversation as resolved.
Show resolved Hide resolved
"double_sign": {
"slash_fraction": "0.05",
"jail_duration": "9223372036854775807"
},
"downtime": {
"slash_fraction": "0.0001",
"jail_duration": "600000000000"
}
},
"allowlisted_reward_denoms": {
"denoms": ["ibc/...", "ibc/..."]
}
Expand Down Expand Up @@ -291,7 +301,7 @@ The parameters not provided are set to their zero value.
}

msg, err := types.NewMsgCreateConsumer(submitter, consCreate.ChainId, consCreate.Metadata, consCreate.InitializationParameters,
consCreate.PowerShapingParameters, consCreate.AllowlistedRewardDenoms)
consCreate.PowerShapingParameters, consCreate.AllowlistedRewardDenoms, consCreate.InfractionParameters)
if err != nil {
return err
}
Expand Down Expand Up @@ -356,6 +366,16 @@ where update_consumer.json has the following structure:
"allow_inactive_vals": false,
"prioritylist": ["cosmosvalcons..."]
},
"infraction_parameters": {
"double_sign": {
"slash_fraction": "0.05",
"jail_duration": "9223372036854775807"
},
"downtime": {
"slash_fraction": "0.0001",
"jail_duration": "600000000000"
}
},
"allowlisted_reward_denoms": {
"denoms": ["ibc/...", "ibc/..."]
}
Expand Down Expand Up @@ -398,7 +418,7 @@ If one of the fields is missing, it will be set to its zero value.
}

msg, err := types.NewMsgUpdateConsumer(owner, consUpdate.ConsumerId, consUpdate.NewOwnerAddress, consUpdate.Metadata,
consUpdate.InitializationParameters, consUpdate.PowerShapingParameters, consUpdate.AllowlistedRewardDenoms, consUpdate.NewChainId)
consUpdate.InitializationParameters, consUpdate.PowerShapingParameters, consUpdate.AllowlistedRewardDenoms, consUpdate.NewChainId, consUpdate.InfractionParameters)
if err != nil {
return err
}
Expand Down
33 changes: 20 additions & 13 deletions x/ccv/provider/keeper/consumer_equivocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (

errorsmod "cosmossdk.io/errors"
"cosmossdk.io/math"
evidencetypes "cosmossdk.io/x/evidence/types"

cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
sdk "github.com/cosmos/cosmos-sdk/types"
Expand Down Expand Up @@ -75,10 +74,16 @@ func (k Keeper) HandleConsumerDoubleVoting(
types.NewConsumerConsAddress(sdk.ConsAddress(evidence.VoteA.ValidatorAddress.Bytes())),
)

if err = k.SlashValidator(ctx, providerAddr); err != nil {
// get the consumer's infraction parameters
infractionParams, err := k.GetInfractionParameters(ctx, consumerId)
if err != nil {
return err
}

if err = k.SlashValidator(ctx, providerAddr, infractionParams.DoubleSign); err != nil {
return err
}
if err = k.JailAndTombstoneValidator(ctx, providerAddr); err != nil {
if err = k.JailAndTombstoneValidator(ctx, providerAddr, infractionParams.DoubleSign); err != nil {
return err
}

Expand Down Expand Up @@ -186,19 +191,24 @@ func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, consumerId string, m

provAddrs := make([]types.ProviderConsAddress, 0, len(byzantineValidators))

infractionParams, err := k.GetInfractionParameters(ctx, consumerId)
if err != nil {
return err
}

// slash, jail, and tombstone the Byzantine validators
for _, v := range byzantineValidators {
providerAddr := k.GetProviderAddrFromConsumerAddr(
ctx,
consumerId,
types.NewConsumerConsAddress(sdk.ConsAddress(v.Address.Bytes())),
)
err := k.SlashValidator(ctx, providerAddr)
err := k.SlashValidator(ctx, providerAddr, infractionParams.DoubleSign)
if err != nil {
logger.Error("failed to slash validator: %s", err)
continue
}
err = k.JailAndTombstoneValidator(ctx, providerAddr)
err = k.JailAndTombstoneValidator(ctx, providerAddr, infractionParams.DoubleSign)
// JailAndTombstoneValidator should never return an error if
// SlashValidator succeeded because both methods fail if the malicious
// validator is either or both !found, unbonded and tombstoned.
Expand Down Expand Up @@ -411,7 +421,7 @@ func verifyLightBlockCommitSig(lightBlock tmtypes.LightBlock, sigIdx int) error
//

// JailAndTombstoneValidator jails and tombstones the validator with the given provider consensus address
func (k Keeper) JailAndTombstoneValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress) error {
func (k Keeper) JailAndTombstoneValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress, jailingParams *types.SlashJailParameters) error {
validator, err := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr())
if err != nil && errors.Is(err, stakingtypes.ErrNoValidatorFound) {
return errorsmod.Wrapf(slashingtypes.ErrNoValidatorForAddress, "provider consensus address: %s", providerAddr.String())
Expand All @@ -435,7 +445,8 @@ func (k Keeper) JailAndTombstoneValidator(ctx sdk.Context, providerAddr types.Pr
}
}

err = k.slashingKeeper.JailUntil(ctx, providerAddr.ToSdkConsAddr(), evidencetypes.DoubleSignJailEndTime)
jailEndTime := ctx.BlockTime().Add(jailingParams.JailDuration)
err = k.slashingKeeper.JailUntil(ctx, providerAddr.ToSdkConsAddr(), jailEndTime)
if err != nil {
return fmt.Errorf("fail to set jail duration for validator: %s: %s", providerAddr.String(), err)
}
Expand Down Expand Up @@ -481,7 +492,7 @@ func (k Keeper) ComputePowerToSlash(ctx sdk.Context, validator stakingtypes.Vali
}

// SlashValidator slashes validator with given provider Address
func (k Keeper) SlashValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress) error {
func (k Keeper) SlashValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress, slashingParams *types.SlashJailParameters) error {
validator, err := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr())
if err != nil && errors.Is(err, stakingtypes.ErrNoValidatorFound) {
return errorsmod.Wrapf(slashingtypes.ErrNoValidatorForAddress, "provider consensus address: %s", providerAddr.String())
Expand Down Expand Up @@ -518,16 +529,12 @@ func (k Keeper) SlashValidator(ctx sdk.Context, providerAddr types.ProviderConsA
powerReduction := k.stakingKeeper.PowerReduction(ctx)
totalPower := k.ComputePowerToSlash(ctx, validator, undelegations, redelegations, lastPower, powerReduction)

slashFraction, err := k.slashingKeeper.SlashFractionDoubleSign(ctx)
if err != nil {
return err
}
consAdrr, err := validator.GetConsAddr()
if err != nil {
return err
}

_, err = k.stakingKeeper.SlashWithInfractionReason(ctx, consAdrr, 0, totalPower, slashFraction, stakingtypes.Infraction_INFRACTION_DOUBLE_SIGN)
_, err = k.stakingKeeper.SlashWithInfractionReason(ctx, consAdrr, 0, totalPower, slashingParams.SlashFraction, stakingtypes.Infraction_INFRACTION_DOUBLE_SIGN)
return err
}

Expand Down
Loading
Loading