Skip to content

Commit

Permalink
add tests that checks that Slash is called
Browse files Browse the repository at this point in the history
  • Loading branch information
insumity committed Sep 11, 2023
1 parent 584fec2 commit 463bfd6
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 33 deletions.
2 changes: 1 addition & 1 deletion tests/integration/double_vote.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

// TestHandleConsumerDoubleVoting verifies that handling a double voting evidence
// of a consumer chain results in the expected jailing of the malicious validator
// of a consumer chain results in the expected tombstoning and jailing of the malicious validator
func (s *CCVTestSuite) TestHandleConsumerDoubleVoting() {
s.SetupCCVChannel(s.path)
// required to have the consumer client revision height greater than 0
Expand Down
13 changes: 12 additions & 1 deletion x/ccv/provider/keeper/double_vote.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"bytes"
"fmt"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
"github.com/cosmos/cosmos-sdk/x/evidence/exported"
evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
Expand Down Expand Up @@ -32,10 +34,19 @@ func (k Keeper) HandleConsumerDoubleVoting(
types.NewConsumerConsAddress(sdk.ConsAddress(evidence.VoteA.ValidatorAddress.Bytes())),
)

// execute the jailing
k.SlashValidator(ctx, providerAddr)
k.JailAndTombstoneValidator(ctx, providerAddr)

// verify the following values
var equivocation exported.Evidence = &evidencetypes.Equivocation{
Height: evidence.Height(),
Time: evidence.Time(),
Power: evidence.ValidatorPower,
ConsensusAddress: evidence.VoteA.ValidatorAddress.String(),
}

k.evidenceKeeper.SetEvidence(ctx, equivocation)

k.Logger(ctx).Info(
"confirmed equivocation",
"byzantine validator address", providerAddr.String(),
Expand Down
17 changes: 15 additions & 2 deletions x/ccv/provider/keeper/misbehaviour.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package keeper

import (
"github.com/cosmos/interchain-security/v2/x/ccv/provider/types"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/evidence/exported"
evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types"
ibcclienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types"
ibctmtypes "github.com/cosmos/ibc-go/v4/modules/light-clients/07-tendermint/types"
"github.com/cosmos/interchain-security/v2/x/ccv/provider/types"
tmtypes "github.com/tendermint/tendermint/types"
)

Expand Down Expand Up @@ -46,6 +47,18 @@ func (k Keeper) HandleConsumerMisbehaviour(ctx sdk.Context, misbehaviour ibctmty
provAddrs = append(provAddrs, providerAddr)
}

// FIXME: it would be nice to set the evidence but not sure what this would mean
// for light client attacks.
var evidence exported.Evidence = &evidencetypes.Equivocation{
// FIXME
Height: int64(misbehaviour.Header1.GetHeight().GetRevisionHeight()),
Time: misbehaviour.Header2.GetTime(),
Power: 0, // misbehaviour.Header1.ValidatorSet.Validators
ConsensusAddress: "hola!", //misbehaviour.Header1.ValidatorSet.Proposer.Address
}

k.evidenceKeeper.SetEvidence(ctx, evidence)

logger.Info(
"confirmed equivocation light client attack",
"byzantine validators", provAddrs,
Expand Down
63 changes: 38 additions & 25 deletions x/ccv/provider/keeper/punish_validator.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package keeper

import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
Expand Down Expand Up @@ -38,52 +39,64 @@ func (k Keeper) JailAndTombstoneValidator(ctx sdk.Context, providerAddr types.Pr
// Note that we cannot simply use the fact that a validator is jailed to avoid slashing more than once
// because then a validator could i) perform an equivocation, ii) get jailed (e.g., through downtime)
// and in such a case the validator would not get slashed when calling `SlashValidator`.
// TODO: check if tombstone ... can it panic???
k.slashingKeeper.Tombstone(ctx, providerAddr.ToSdkConsAddr())

//k.evidenceKeeper.SetEvidence(ctx)
}

// Slash validator based on the `providerAddr`
func (k Keeper) SlashValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress) {
logger := k.Logger(ctx)

val, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr())
if !found {
logger.Error("validator not found", "provider consensus address", providerAddr.String())
return
}

if k.slashingKeeper.IsTombstoned(ctx, providerAddr.ToSdkConsAddr()) {
logger.Info("validator is already tombstoned", "provider consensus address", providerAddr.String())
return
}

valOperatorAddress := val.GetOperator()
func (k Keeper) ComputePowerToSlash(undelegations []stakingtypes.UnbondingDelegation,
redelegations []stakingtypes.Redelegation, power int64, powerReduction sdk.Int) int64 {

// compute the total numbers of tokens currently being undelegated
undelegationsInTokens := sdk.NewInt(0)
for _, v := range k.stakingKeeper.GetUnbondingDelegationsFromValidator(ctx, valOperatorAddress) {
for _, entry := range v.Entries {
for _, u := range undelegations {
for _, entry := range u.Entries {
undelegationsInTokens = undelegationsInTokens.Add(entry.InitialBalance)
}
}

// compute the total numbers of tokens currently being redelegated
redelegationsInTokens := sdk.NewInt(0)
for _, v := range k.stakingKeeper.GetRedelegationsFromSrcValidator(ctx, valOperatorAddress) {
for _, entry := range v.Entries {
for _, r := range redelegations {
for _, entry := range r.Entries {
redelegationsInTokens = redelegationsInTokens.Add(entry.InitialBalance)
}
}

// The power we pass to staking's keeper `Slash` method is the current power of the validator together with the total
// power of all the currently undelegated and redelegated tokens (see docs/docs/adrs/adr-013-equivocation-slashing.md).
powerReduction := k.stakingKeeper.PowerReduction(ctx)
undelegationsAndRedelegationsInPower := sdk.TokensToConsensusPower(
undelegationsInTokens.Add(redelegationsInTokens), powerReduction)

power := k.stakingKeeper.GetLastValidatorPower(ctx, valOperatorAddress)
totalPower := power + undelegationsAndRedelegationsInPower
return power + undelegationsAndRedelegationsInPower
}

// Slash validator based on the `providerAddr`
func (k Keeper) SlashValidator(ctx sdk.Context, providerAddr types.ProviderConsAddress) {
logger := k.Logger(ctx)

fmt.Println(">>>>")
fmt.Println(ctx)
fmt.Println("===")
fmt.Println(providerAddr)
fmt.Println(providerAddr.ToSdkConsAddr().Bytes())
fmt.Println(">>>>")
val, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, providerAddr.ToSdkConsAddr())
if !found {
logger.Error("validator not found", "provider consensus address", providerAddr.String())
return
}

if k.slashingKeeper.IsTombstoned(ctx, providerAddr.ToSdkConsAddr()) {
logger.Info("validator is already tombstoned", "provider consensus address", providerAddr.String())
return
}

undelegations := k.stakingKeeper.GetUnbondingDelegationsFromValidator(ctx, val.GetOperator())
redelegations := k.stakingKeeper.GetRedelegationsFromSrcValidator(ctx, val.GetOperator())
lastPower := k.stakingKeeper.GetLastValidatorPower(ctx, val.GetOperator())
powerReduction := k.stakingKeeper.PowerReduction(ctx)
totalPower := k.ComputePowerToSlash(undelegations, redelegations, lastPower, powerReduction)

slashFraction := k.slashingKeeper.SlashFractionDoubleSign(ctx)

k.stakingKeeper.Slash(ctx, providerAddr.ToSdkConsAddr(), 0, totalPower, slashFraction, stakingtypes.DoubleSign)
Expand Down
201 changes: 197 additions & 4 deletions x/ccv/provider/keeper/punish_validator_test.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
package keeper_test

import (
"testing"

"fmt"
cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
cryptotestutil "github.com/cosmos/interchain-security/v2/testutil/crypto"
testkeeper "github.com/cosmos/interchain-security/v2/testutil/keeper"
"github.com/cosmos/interchain-security/v2/x/ccv/provider/types"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
tmtypes "github.com/tendermint/tendermint/types"
"testing"
"time"
)

// TestJailValidator tests that the jailing of a validator is only executed
// FIXME: break the JailAndTombstoneValidator function into two ... seems complicated to do both at once
// TestJailAndTombstoneValidator tests that the jailing of a validator is only executed
// under the conditions that the validator is neither unbonded, already jailed, nor tombstoned.
func TestJailValidator(t *testing.T) {
func TestJailAndTombstoneValidator(t *testing.T) {
providerConsAddr := cryptotestutil.NewCryptoIdentityFromIntSeed(7842334).ProviderConsAddress()
testCases := []struct {
name string
Expand Down Expand Up @@ -130,3 +135,191 @@ func TestJailValidator(t *testing.T) {
ctrl.Finish()
}
}

func createUndelegation(tokensPerEntry []int64) stakingtypes.UnbondingDelegation {
var entries []stakingtypes.UnbondingDelegationEntry
for _, t := range tokensPerEntry {
entry := stakingtypes.UnbondingDelegationEntry{
InitialBalance: sdk.NewInt(t),
}
entries = append(entries, entry)
}

return stakingtypes.UnbondingDelegation{Entries: entries}
}

func createRedelegation(tokensPerEntry []int64) stakingtypes.Redelegation {
var entries []stakingtypes.RedelegationEntry
for _, t := range tokensPerEntry {
entry := stakingtypes.RedelegationEntry{
InitialBalance: sdk.NewInt(t),
}
entries = append(entries, entry)
}

return stakingtypes.Redelegation{Entries: entries}
}

func TestComputePowerToSlash(t *testing.T) {
providerKeeper, _, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t))
defer ctrl.Finish()

testCases := []struct {
name string
undelegations []stakingtypes.UnbondingDelegation
redelegations []stakingtypes.Redelegation
power int64
powerReduction sdk.Int
expectedPower int64
}{
{
"both undelegations and redelegations 1",
// 1000 total undelegation tokens
[]stakingtypes.UnbondingDelegation{
createUndelegation([]int64{250, 250}),
createUndelegation([]int64{500})},
// 1000 total redelegation tokens
[]stakingtypes.Redelegation{
createRedelegation([]int64{500}),
createRedelegation([]int64{250, 250}),
},
int64(1000),
sdk.NewInt(1),
int64(2000/1 + 1000),
},
{
"both undelegations and redelegations 2",
// 2000 total undelegation tokens
[]stakingtypes.UnbondingDelegation{
createUndelegation([]int64{250, 250}),
createUndelegation([]int64{}),
createUndelegation([]int64{100, 100}),
createUndelegation([]int64{800}),
createUndelegation([]int64{500})},
// 3500 total redelegation tokens
[]stakingtypes.Redelegation{
createRedelegation([]int64{}),
createRedelegation([]int64{1600}),
createRedelegation([]int64{350, 250}),
createRedelegation([]int64{700, 200}),
createRedelegation([]int64{}),
createRedelegation([]int64{400}),
},
int64(8391),
sdk.NewInt(2),
int64((2000+3500)/2 + 8391),
},
{
"no undelegations or redelegations, return provided power",
[]stakingtypes.UnbondingDelegation{},
[]stakingtypes.Redelegation{},
int64(3000),
sdk.NewInt(5),
int64(0/5 + 3000),
},
{
"no undelegations",
[]stakingtypes.UnbondingDelegation{},
// 2000 total redelegation tokens
[]stakingtypes.Redelegation{
createRedelegation([]int64{}),
createRedelegation([]int64{500}),
createRedelegation([]int64{250, 250}),
createRedelegation([]int64{700, 200}),
createRedelegation([]int64{}),
createRedelegation([]int64{100}),
},
int64(17),
sdk.NewInt(3),
int64(2000/3 + 17),
},
{
"no redelegations",
// 2000 total undelegation tokens
[]stakingtypes.UnbondingDelegation{
createUndelegation([]int64{250, 250}),
createUndelegation([]int64{}),
createUndelegation([]int64{100, 100}),
createUndelegation([]int64{800}),
createUndelegation([]int64{500})},
[]stakingtypes.Redelegation{},
int64(1),
sdk.NewInt(3),
int64(2000/3 + 1),
},
}

for _, tc := range testCases {
actualPower := providerKeeper.ComputePowerToSlash(tc.undelegations, tc.redelegations, tc.power, tc.powerReduction)
if tc.expectedPower != actualPower {
require.Fail(t, fmt.Sprintf("\"%s\" failed", tc.name),
"expected is %d but actual is %d", tc.expectedPower, actualPower)
}
}
}

// TestSlashValidator asserts that `SlashValidator` calls the staking module's `Slash` method
// with the correct arguments (i.e., `infractionHeight` of 0 and the expected slash power)
func TestSlashValidator(t *testing.T) {
keeper, ctx, ctrl, mocks := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t))
defer ctrl.Finish()

ctx = ctx.WithBlockTime(time.Now())
keeperParams := testkeeper.NewInMemKeeperParams(t)
testkeeper.NewInMemProviderKeeper(keeperParams, mocks)

pubKey, _ := cryptocodec.FromTmPubKeyInterface(tmtypes.NewMockPV().PrivKey.PubKey())

validator, _ := stakingtypes.NewValidator(pubKey.Address().Bytes(), pubKey, stakingtypes.Description{})
consAddr, _ := validator.GetConsAddr()
providerAddr := types.NewProviderConsAddress(consAddr)

// we create 1000 tokens worth of undelegations and 1000 tokens worth of redelegations
undelegations := []stakingtypes.UnbondingDelegation{
createUndelegation([]int64{250, 250}),
createUndelegation([]int64{500})}
redelegations := []stakingtypes.Redelegation{
createRedelegation([]int64{250, 250}),
createRedelegation([]int64{500})}

// validator's current power
currentPower := int64(3000)

powerReduction := sdk.NewInt(2)
slashFraction, _ := sdk.NewDecFromStr("0.5")

// the call to `Slash` should provide an `infractionHeight` of 0 and an expected power of
// (1000 (undelegations) + 1000 (redelegations)) / 2 (= powerReduction) + 3000 (currentPower) = 4000
expectedInfractionHeight := int64(0)
expectedSlashPower := int64(4000)

expectedCalls := []*gomock.Call{
mocks.MockStakingKeeper.EXPECT().
GetValidatorByConsAddr(ctx, gomock.Any()).
Return(validator, true),
mocks.MockSlashingKeeper.EXPECT().
IsTombstoned(ctx, consAddr).
Return(false),
mocks.MockStakingKeeper.EXPECT().
GetUnbondingDelegationsFromValidator(ctx, validator.GetOperator()).
Return(undelegations),
mocks.MockStakingKeeper.EXPECT().
GetRedelegationsFromSrcValidator(ctx, validator.GetOperator()).
Return(redelegations),
mocks.MockStakingKeeper.EXPECT().
GetLastValidatorPower(ctx, validator.GetOperator()).
Return(currentPower),
mocks.MockStakingKeeper.EXPECT().
PowerReduction(ctx).
Return(powerReduction),
mocks.MockSlashingKeeper.EXPECT().
SlashFractionDoubleSign(ctx).
Return(slashFraction),
mocks.MockStakingKeeper.EXPECT().
Slash(ctx, consAddr, expectedInfractionHeight, expectedSlashPower, slashFraction, stakingtypes.DoubleSign).
Times(1),
}

gomock.InOrder(expectedCalls...)
keeper.SlashValidator(ctx, providerAddr)
}

0 comments on commit 463bfd6

Please sign in to comment.