diff --git a/tests/e2e/throttle.go b/tests/e2e/throttle.go index d101d7addf..db1fbdbe99 100644 --- a/tests/e2e/throttle.go +++ b/tests/e2e/throttle.go @@ -360,7 +360,6 @@ func (s *CCVTestSuite) TestDoubleSignDoesNotAffectThrottling() { // Track and increment ibc seq num for each packet, since these need to be unique. ibcSeqNum := uint64(0) - // Construct 500 double-sign slash packets for ibcSeqNum < 500 { // Increment ibc seq num for each packet (starting at 1) @@ -405,6 +404,13 @@ func (s *CCVTestSuite) TestDoubleSignDoesNotAffectThrottling() { s.Require().Fail("validator not found") } s.Require().False(stakingVal.Jailed) + + // 4th validator should have no slash log, all the others do + if val != s.providerChain.Vals.Validators[3] { + s.Require().True(providerKeeper.GetSlashLog(s.providerCtx(), sdk.ConsAddress(val.Address))) + } else { + s.Require().False(providerKeeper.GetSlashLog(s.providerCtx(), sdk.ConsAddress(val.Address))) + } } } diff --git a/tests/integration/steps.go b/tests/integration/steps.go index ff18c685e3..de91a744f2 100644 --- a/tests/integration/steps.go +++ b/tests/integration/steps.go @@ -20,7 +20,9 @@ var happyPathSteps = concatSteps( stepsUnbond("consu"), stepsRedelegate("consu"), stepsDowntime("consu"), - stepsSubmitEquivocationProposal("consu", 2), + stepsRejectEquivocationProposal("consu", 2), // prop to tombstone bob is rejected + stepsDoubleSignOnProviderAndConsumer("consu"), // carol double signs on provider, bob double signs on consumer + stepsSubmitEquivocationProposal("consu", 2), // now prop to tombstone bob is submitted and accepted stepsStopChain("consu", 3), ) @@ -46,5 +48,5 @@ var multipleConsumers = concatSteps( stepsMultiConsumerRedelegate("consu", "densu"), stepsMultiConsumerDowntimeFromConsumer("consu", "densu"), stepsMultiConsumerDowntimeFromProvider("consu", "densu"), - stepsDoubleSign("consu", "densu"), // double sign on one of the chains + stepsMultiConsumerDoubleSign("consu", "densu"), // double sign on one of the chains ) diff --git a/tests/integration/steps_double_sign.go b/tests/integration/steps_double_sign.go index 8ec32e835f..e4efca4bb5 100644 --- a/tests/integration/steps_double_sign.go +++ b/tests/integration/steps_double_sign.go @@ -1,15 +1,7 @@ package main -// simulates double signing on provider and vsc propagation to consumer chains -// -// Note: These steps would be affected by slash packet throttling, since the -// consumer-initiated slash steps are executed after consumer-initiated downtime -// slashes have already occurred. However slash packet throttling is -// psuedo-disabled in this test by setting the slash meter replenish -// fraction to 1.0 in the config file. -// -// only double sign on provider chain will cause slashing and tombstoning -func stepsDoubleSign(consumer1, consumer2 string) []Step { +// Steps that make carol double sign on the provider, and bob double sign on a single consumer +func stepsDoubleSignOnProviderAndConsumer(consumerName string) []Step { return []Step{ { // provider double sign @@ -26,28 +18,21 @@ func stepsDoubleSign(consumer1, consumer2 string) []Step { validatorID("carol"): 0, // from 500 to 0 }, }, - chainID(consumer1): ChainState{ + chainID(consumerName): ChainState{ ValPowers: &map[validatorID]uint{ validatorID("alice"): 509, validatorID("bob"): 500, - validatorID("carol"): 495, // not tombstoned on consumer1 yet - }, - }, - chainID(consumer2): ChainState{ - ValPowers: &map[validatorID]uint{ - validatorID("alice"): 509, - validatorID("bob"): 500, - validatorID("carol"): 495, // not tombstoned on consumer2 yet + validatorID("carol"): 495, // not tombstoned on consumerName yet }, }, }, }, { - // relay power change to consumer1 + // relay power change to consumerName action: relayPacketsAction{ chain: chainID("provi"), port: "provider", - channel: 0, // consumer1 channel + channel: 0, // consumerName channel }, state: State{ chainID("provi"): ChainState{ @@ -57,56 +42,19 @@ func stepsDoubleSign(consumer1, consumer2 string) []Step { validatorID("carol"): 0, }, }, - chainID(consumer1): ChainState{ - ValPowers: &map[validatorID]uint{ - validatorID("alice"): 509, - validatorID("bob"): 500, - validatorID("carol"): 0, // tombstoning visible on consumer1 - }, - }, - chainID(consumer2): ChainState{ + chainID(consumerName): ChainState{ ValPowers: &map[validatorID]uint{ validatorID("alice"): 509, validatorID("bob"): 500, - validatorID("carol"): 495, // tombstoning NOT YET visible on consumer2 - }, - }, - }, - }, - { - // relay power change to consumer2 - action: relayPacketsAction{ - chain: chainID("provi"), - port: "provider", - channel: 1, // consumer2 channel - }, - state: State{ - chainID("provi"): ChainState{ - ValPowers: &map[validatorID]uint{ - validatorID("alice"): 509, - validatorID("bob"): 500, - validatorID("carol"): 0, - }, - }, - chainID(consumer1): ChainState{ - ValPowers: &map[validatorID]uint{ - validatorID("alice"): 509, - validatorID("bob"): 500, - validatorID("carol"): 0, - }, - }, - chainID(consumer2): ChainState{ - ValPowers: &map[validatorID]uint{ - validatorID("alice"): 509, - validatorID("bob"): 500, - validatorID("carol"): 0, // tombstoned on consumer2 + validatorID("carol"): 0, // tombstoning visible on consumerName }, }, }, }, { // consumer double sign - // nothing should happen - double sign from consumer is dropped + // provider will only log the double sign slash + // stepsSubmitEquivocationProposal will cause the double sign slash to be executed action: doublesignSlashAction{ chain: chainID("consu"), validator: validatorID("bob"), @@ -119,14 +67,7 @@ func stepsDoubleSign(consumer1, consumer2 string) []Step { validatorID("carol"): 0, }, }, - chainID(consumer1): ChainState{ - ValPowers: &map[validatorID]uint{ - validatorID("alice"): 509, - validatorID("bob"): 500, - validatorID("carol"): 0, - }, - }, - chainID(consumer2): ChainState{ + chainID(consumerName): ChainState{ ValPowers: &map[validatorID]uint{ validatorID("alice"): 509, validatorID("bob"): 500, @@ -139,7 +80,7 @@ func stepsDoubleSign(consumer1, consumer2 string) []Step { action: relayPacketsAction{ chain: chainID("provi"), port: "provider", - channel: 0, // consumer1 channel + channel: 0, }, state: State{ chainID("provi"): ChainState{ @@ -149,14 +90,7 @@ func stepsDoubleSign(consumer1, consumer2 string) []Step { validatorID("carol"): 0, }, }, - chainID(consumer1): ChainState{ - ValPowers: &map[validatorID]uint{ - validatorID("alice"): 509, - validatorID("bob"): 500, // not tombstoned - validatorID("carol"): 0, - }, - }, - chainID(consumer2): ChainState{ + chainID(consumerName): ChainState{ ValPowers: &map[validatorID]uint{ validatorID("alice"): 509, validatorID("bob"): 500, // not tombstoned @@ -166,28 +100,21 @@ func stepsDoubleSign(consumer1, consumer2 string) []Step { }, }, { - // consumer1 learns about the double sign + // consumer learns about the double sign action: relayPacketsAction{ chain: chainID("provi"), port: "provider", - channel: 0, // consumer1 channel + channel: 0, }, state: State{ chainID("provi"): ChainState{ ValPowers: &map[validatorID]uint{ validatorID("alice"): 509, validatorID("bob"): 500, - validatorID("carol"): 0, // not tombstoned - }, - }, - chainID(consumer1): ChainState{ - ValPowers: &map[validatorID]uint{ - validatorID("alice"): 509, - validatorID("bob"): 500, // not tombstoned validatorID("carol"): 0, }, }, - chainID(consumer2): ChainState{ + chainID(consumerName): ChainState{ ValPowers: &map[validatorID]uint{ validatorID("alice"): 509, validatorID("bob"): 500, // not tombstoned @@ -196,29 +123,5 @@ func stepsDoubleSign(consumer1, consumer2 string) []Step { }, }, }, - { - // consumer2 learns about the double sign - action: relayPacketsAction{ - chain: chainID("provi"), - port: "provider", - channel: 1, // consumer2 channel - }, - state: State{ - chainID(consumer1): ChainState{ - ValPowers: &map[validatorID]uint{ - validatorID("alice"): 509, - validatorID("bob"): 500, - validatorID("carol"): 0, - }, - }, - chainID(consumer2): ChainState{ - ValPowers: &map[validatorID]uint{ - validatorID("alice"): 509, - validatorID("bob"): 500, - validatorID("carol"): 0, - }, - }, - }, - }, } } diff --git a/tests/integration/steps_multi_consumer_double_sign.go b/tests/integration/steps_multi_consumer_double_sign.go new file mode 100644 index 0000000000..9d7859605d --- /dev/null +++ b/tests/integration/steps_multi_consumer_double_sign.go @@ -0,0 +1,224 @@ +package main + +// simulates double signing on provider and vsc propagation to consumer chains +// +// Note: These steps would be affected by slash packet throttling, since the +// consumer-initiated slash steps are executed after consumer-initiated downtime +// slashes have already occurred. However slash packet throttling is +// psuedo-disabled in this test by setting the slash meter replenish +// fraction to 1.0 in the config file. +// +// only double sign on provider chain will cause slashing and tombstoning +func stepsMultiConsumerDoubleSign(consumer1, consumer2 string) []Step { + return []Step{ + { + // provider double sign + action: doublesignSlashAction{ + chain: chainID("provi"), + validator: validatorID("carol"), + }, + state: State{ + // slash on provider + chainID("provi"): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 0, // from 500 to 0 + }, + }, + chainID(consumer1): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 495, // not tombstoned on consumer1 yet + }, + }, + chainID(consumer2): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 495, // not tombstoned on consumer2 yet + }, + }, + }, + }, + { + // relay power change to consumer1 + action: relayPacketsAction{ + chain: chainID("provi"), + port: "provider", + channel: 0, // consumer1 channel + }, + state: State{ + chainID("provi"): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 0, + }, + }, + chainID(consumer1): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 0, // tombstoning visible on consumer1 + }, + }, + chainID(consumer2): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 495, // tombstoning NOT YET visible on consumer2 + }, + }, + }, + }, + { + // relay power change to consumer2 + action: relayPacketsAction{ + chain: chainID("provi"), + port: "provider", + channel: 1, // consumer2 channel + }, + state: State{ + chainID("provi"): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 0, + }, + }, + chainID(consumer1): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 0, + }, + }, + chainID(consumer2): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 0, // tombstoned on consumer2 + }, + }, + }, + }, + { + // consumer double sign + // nothing should happen - double sign from consumer is dropped + action: doublesignSlashAction{ + chain: chainID("consu"), + validator: validatorID("bob"), + }, + state: State{ + chainID("provi"): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 0, + }, + }, + chainID(consumer1): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 0, + }, + }, + chainID(consumer2): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 0, + }, + }, + }, + }, + { + action: relayPacketsAction{ + chain: chainID("provi"), + port: "provider", + channel: 0, // consumer1 channel + }, + state: State{ + chainID("provi"): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, // not tombstoned + validatorID("carol"): 0, + }, + }, + chainID(consumer1): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, // not tombstoned + validatorID("carol"): 0, + }, + }, + chainID(consumer2): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, // not tombstoned + validatorID("carol"): 0, + }, + }, + }, + }, + { + // consumer1 learns about the double sign + action: relayPacketsAction{ + chain: chainID("provi"), + port: "provider", + channel: 0, // consumer1 channel + }, + state: State{ + chainID("provi"): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 0, + }, + }, + chainID(consumer1): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, // not tombstoned + validatorID("carol"): 0, + }, + }, + chainID(consumer2): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, // not tombstoned + validatorID("carol"): 0, + }, + }, + }, + }, + { + // consumer2 learns about the double sign + action: relayPacketsAction{ + chain: chainID("provi"), + port: "provider", + channel: 1, // consumer2 channel + }, + state: State{ + chainID(consumer1): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 0, + }, + }, + chainID(consumer2): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 0, + }, + }, + }, + }, + } +} diff --git a/tests/integration/steps_submit_equivocation_proposal.go b/tests/integration/steps_submit_equivocation_proposal.go index c0886a62c2..f8a91d5362 100644 --- a/tests/integration/steps_submit_equivocation_proposal.go +++ b/tests/integration/steps_submit_equivocation_proposal.go @@ -2,6 +2,47 @@ package main import "time" +// submits an invalid equivocation proposal which should be rejected +func stepsRejectEquivocationProposal(consumerName string, propNumber uint) []Step { + return []Step{ + { + // bob submits a proposal to slash himself + action: submitEquivocationProposalAction{ + chain: chainID("consu"), + from: validatorID("bob"), + deposit: 10000001, + height: 10, + time: time.Now(), + power: 500, + validator: validatorID("bob"), + }, + state: State{ + chainID("provi"): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 495, + }, + ValBalances: &map[validatorID]uint{ + validatorID("bob"): 9500000000, + }, + Proposals: &map[uint]Proposal{ + // proposal does not exist + propNumber: TextProposal{}, + }, + }, + chainID(consumerName): ChainState{ + ValPowers: &map[validatorID]uint{ + validatorID("alice"): 509, + validatorID("bob"): 500, + validatorID("carol"): 495, + }, + }, + }, + }, + } +} + // submits an equivocation proposal, votes on it, and tomstones the equivocating validator func stepsSubmitEquivocationProposal(consumerName string, propNumber uint) []Step { s := []Step{ @@ -21,7 +62,7 @@ func stepsSubmitEquivocationProposal(consumerName string, propNumber uint) []Ste ValPowers: &map[validatorID]uint{ validatorID("alice"): 509, validatorID("bob"): 500, - validatorID("carol"): 495, + validatorID("carol"): 0, }, ValBalances: &map[validatorID]uint{ validatorID("bob"): 9489999999, @@ -40,7 +81,7 @@ func stepsSubmitEquivocationProposal(consumerName string, propNumber uint) []Ste ValPowers: &map[validatorID]uint{ validatorID("alice"): 509, validatorID("bob"): 500, - validatorID("carol"): 495, + validatorID("carol"): 0, }, }, }, @@ -56,8 +97,8 @@ func stepsSubmitEquivocationProposal(consumerName string, propNumber uint) []Ste chainID("provi"): ChainState{ ValPowers: &map[validatorID]uint{ validatorID("alice"): 509, - validatorID("bob"): 0, // bob is slashed after proposal passes - validatorID("carol"): 495, + validatorID("bob"): 0, // bob is tombstoned after proposal passes + validatorID("carol"): 0, }, Proposals: &map[uint]Proposal{ propNumber: EquivocationProposal{ @@ -73,7 +114,7 @@ func stepsSubmitEquivocationProposal(consumerName string, propNumber uint) []Ste ValPowers: &map[validatorID]uint{ validatorID("alice"): 509, validatorID("bob"): 500, // slash not reflected in consumer chain - validatorID("carol"): 495, + validatorID("carol"): 0, }, }, }, @@ -90,14 +131,14 @@ func stepsSubmitEquivocationProposal(consumerName string, propNumber uint) []Ste ValPowers: &map[validatorID]uint{ validatorID("alice"): 509, validatorID("bob"): 0, - validatorID("carol"): 495, + validatorID("carol"): 0, }, }, chainID(consumerName): ChainState{ ValPowers: &map[validatorID]uint{ validatorID("alice"): 509, validatorID("bob"): 0, // slash relayed to consumer chain - validatorID("carol"): 495, + validatorID("carol"): 0, }, }, }, diff --git a/x/ccv/provider/keeper/keeper.go b/x/ccv/provider/keeper/keeper.go index daf45ac749..dc612b110a 100644 --- a/x/ccv/provider/keeper/keeper.go +++ b/x/ccv/provider/keeper/keeper.go @@ -990,3 +990,25 @@ func (k Keeper) GetFirstVscSendTimestamp(ctx sdk.Context, chainID string) (vscSe return types.VscSendTimestamp{}, false } + +// SetSlashLog updates validator's slash log for a consumer chain +// If an entry exists for a given validator address, at least one +// double signing slash packet was received by the provider from at least one consumer chain +func (k Keeper) SetSlashLog( + ctx sdk.Context, + providerAddr sdk.ConsAddress, +) { + store := ctx.KVStore(k.storeKey) + store.Set(types.SlashLogKey(providerAddr), []byte{}) +} + +// GetSlashLog returns a validator's slash log status +// True will be returned if an entry exists for a given validator address +func (k Keeper) GetSlashLog( + ctx sdk.Context, + providerAddr sdk.ConsAddress, +) (found bool) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.SlashLogKey(providerAddr)) + return bz != nil +} diff --git a/x/ccv/provider/keeper/keeper_test.go b/x/ccv/provider/keeper/keeper_test.go index dc85f6110a..7127081904 100644 --- a/x/ccv/provider/keeper/keeper_test.go +++ b/x/ccv/provider/keeper/keeper_test.go @@ -10,6 +10,7 @@ import ( ibcsimapp "github.com/cosmos/interchain-security/legacy_ibc_testing/simapp" + cryptotestutil "github.com/cosmos/interchain-security/testutil/crypto" testkeeper "github.com/cosmos/interchain-security/testutil/keeper" "github.com/cosmos/interchain-security/x/ccv/provider/types" ccv "github.com/cosmos/interchain-security/x/ccv/types" @@ -518,3 +519,16 @@ func TestRemoveConsumerFromUnbondingOp(t *testing.T) { require.False(t, canComplete) }) } + +// TestSetSlashLog tests slash log getter and setter methods +func TestSetSlashLog(t *testing.T) { + providerKeeper, ctx, ctrl, _ := testkeeper.GetProviderKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t)) + defer ctrl.Finish() + + addrWithDoubleSigns := cryptotestutil.NewCryptoIdentityFromIntSeed(1).SDKValConsAddress() + addrWithoutDoubleSigns := cryptotestutil.NewCryptoIdentityFromIntSeed(2).SDKValConsAddress() + + providerKeeper.SetSlashLog(ctx, addrWithDoubleSigns) + require.True(t, providerKeeper.GetSlashLog(ctx, addrWithDoubleSigns)) + require.False(t, providerKeeper.GetSlashLog(ctx, addrWithoutDoubleSigns)) +} diff --git a/x/ccv/provider/keeper/proposal.go b/x/ccv/provider/keeper/proposal.go index 4843ca3e4b..30f0c6b12e 100644 --- a/x/ccv/provider/keeper/proposal.go +++ b/x/ccv/provider/keeper/proposal.go @@ -603,8 +603,12 @@ func (k Keeper) StopConsumerChainInCachedCtx(ctx sdk.Context, p types.ConsumerRe } // HandleEquivocationProposal handles an equivocation proposal. +// Proposal will be accepted if a record in the SlashLog exists for a given validator address. func (k Keeper) HandleEquivocationProposal(ctx sdk.Context, p *types.EquivocationProposal) error { for _, ev := range p.Equivocations { + if !k.GetSlashLog(ctx, ev.GetConsensusAddress()) { + return fmt.Errorf("no equivocation record found for validator %s", ev.GetConsensusAddress().String()) + } k.evidenceKeeper.HandleEquivocationEvidence(ctx, ev) } return nil diff --git a/x/ccv/provider/keeper/proposal_test.go b/x/ccv/provider/keeper/proposal_test.go index d1b48c6efe..5b32663892 100644 --- a/x/ccv/provider/keeper/proposal_test.go +++ b/x/ccv/provider/keeper/proposal_test.go @@ -1004,25 +1004,53 @@ func TestBeginBlockCCR(t *testing.T) { func TestHandleEquivocationProposal(t *testing.T) { keeperParams := testkeeper.NewInMemKeeperParams(t) keeper, ctx, _, mocks := testkeeper.GetProviderKeeperAndCtx(t, keeperParams) - equivocation1 := &evidencetypes.Equivocation{ - Time: time.Now(), - Height: 1, - Power: 1, - ConsensusAddress: "addr1", + tcFail := []*evidencetypes.Equivocation{ + &evidencetypes.Equivocation{ + Time: time.Now(), + Height: 1, + Power: 1, + ConsensusAddress: "addr1", + }, + &evidencetypes.Equivocation{ + Time: time.Now(), + Height: 1, + Power: 1, + ConsensusAddress: "addr2", + }, + } + + tcPass := []*evidencetypes.Equivocation{ + &evidencetypes.Equivocation{ + Time: time.Now(), + Height: 1, + Power: 1, + ConsensusAddress: "addr3", + }, + &evidencetypes.Equivocation{ + Time: time.Now(), + Height: 1, + Power: 1, + ConsensusAddress: "addr4", + }, } - equivocation2 := &evidencetypes.Equivocation{ - Time: time.Now(), - Height: 1, - Power: 1, - ConsensusAddress: "addr2", + + // test failing prop + propFail := &providertypes.EquivocationProposal{ + Equivocations: []*evidencetypes.Equivocation{tcFail[0], tcFail[1]}, } - prop := &providertypes.EquivocationProposal{ - Equivocations: []*evidencetypes.Equivocation{equivocation1, equivocation2}, + err := keeper.HandleEquivocationProposal(ctx, propFail) + require.Error(t, err) + + // test passing prop + propPass := &providertypes.EquivocationProposal{ + Equivocations: []*evidencetypes.Equivocation{tcPass[0], tcPass[1]}, } - mocks.MockEvidenceKeeper.EXPECT().HandleEquivocationEvidence(ctx, equivocation1) - mocks.MockEvidenceKeeper.EXPECT().HandleEquivocationEvidence(ctx, equivocation2) + keeper.SetSlashLog(ctx, tcPass[0].GetConsensusAddress()) + keeper.SetSlashLog(ctx, tcPass[0].GetConsensusAddress()) - err := keeper.HandleEquivocationProposal(ctx, prop) + mocks.MockEvidenceKeeper.EXPECT().HandleEquivocationEvidence(ctx, tcPass[0]) + mocks.MockEvidenceKeeper.EXPECT().HandleEquivocationEvidence(ctx, tcPass[1]) + err = keeper.HandleEquivocationProposal(ctx, propPass) require.NoError(t, err) } diff --git a/x/ccv/provider/keeper/relay.go b/x/ccv/provider/keeper/relay.go index 6c993ea094..5482d6e45b 100644 --- a/x/ccv/provider/keeper/relay.go +++ b/x/ccv/provider/keeper/relay.go @@ -319,8 +319,8 @@ func (k Keeper) OnRecvSlashPacket(ctx sdk.Context, packet channeltypes.Packet, d // getMappedInfractionHeight is already checked in ValidateSlashPacket infractionHeight, _ := k.getMappedInfractionHeight(ctx, chainID, data.ValsetUpdateId) - // TODO: would be better to have a warning, but there is no Warn() function - k.Logger(ctx).Error("SlashPacket received for double-signing", + k.SetSlashLog(ctx, providerConsAddr) + k.Logger(ctx).Info("SlashPacket received for double-signing", "chainID", chainID, "consumer cons addr", sdk.ConsAddress(data.Validator.Address).String(), "provider cons addr", providerConsAddr.String(), diff --git a/x/ccv/provider/keeper/relay_test.go b/x/ccv/provider/keeper/relay_test.go index 7f7c49b3f5..763c38aaa3 100644 --- a/x/ccv/provider/keeper/relay_test.go +++ b/x/ccv/provider/keeper/relay_test.go @@ -12,6 +12,7 @@ import ( exported "github.com/cosmos/ibc-go/v4/modules/core/exported" ibcsimapp "github.com/cosmos/interchain-security/legacy_ibc_testing/simapp" "github.com/cosmos/interchain-security/testutil/crypto" + cryptotestutil "github.com/cosmos/interchain-security/testutil/crypto" testkeeper "github.com/cosmos/interchain-security/testutil/keeper" "github.com/cosmos/interchain-security/x/ccv/provider/keeper" providertypes "github.com/cosmos/interchain-security/x/ccv/provider/types" @@ -256,6 +257,11 @@ func TestOnRecvDoubleSignSlashPacket(t *testing.T) { require.Equal(t, uint64(0), providerKeeper.GetThrottledPacketDataSize(ctx, "chain-1")) require.Equal(t, uint64(0), providerKeeper.GetThrottledPacketDataSize(ctx, "chain-2")) require.Equal(t, 0, len(providerKeeper.GetAllGlobalSlashEntries(ctx))) + require.True(t, providerKeeper.GetSlashLog(ctx, sdk.ConsAddress(packetData.Validator.Address))) + + // slash log should be empty for a random validator address in this testcase + randomAddress := cryptotestutil.NewCryptoIdentityFromIntSeed(100).SDKValConsAddress() + require.False(t, providerKeeper.GetSlashLog(ctx, randomAddress)) } // TestOnRecvSlashPacket tests the OnRecvSlashPacket method specifically for downtime slash packets, diff --git a/x/ccv/provider/proposal_handler_test.go b/x/ccv/provider/proposal_handler_test.go index 20bc6dcd8e..0929ea90ef 100644 --- a/x/ccv/provider/proposal_handler_test.go +++ b/x/ccv/provider/proposal_handler_test.go @@ -56,6 +56,14 @@ func TestProviderProposalHandler(t *testing.T) { blockTime: hourFromNow, expValidConsumerRemoval: true, }, + { + // no slash log for equivocation + name: "invalid equivocation posal", + content: providertypes.NewEquivocationProposal( + "title", "description", []*evidencetypes.Equivocation{equivocation}), + blockTime: hourFromNow, + expValidEquivocation: false, + }, { name: "valid equivocation posal", content: providertypes.NewEquivocationProposal( @@ -95,6 +103,7 @@ func TestProviderProposalHandler(t *testing.T) { testkeeper.SetupForStoppingConsumerChain(t, ctx, &providerKeeper, mocks) case tc.expValidEquivocation: + providerKeeper.SetSlashLog(ctx, equivocation.GetConsensusAddress()) mocks.MockEvidenceKeeper.EXPECT().HandleEquivocationEvidence(ctx, equivocation) } diff --git a/x/ccv/provider/types/keys.go b/x/ccv/provider/types/keys.go index 198c3913c2..3a3e1151e8 100644 --- a/x/ccv/provider/types/keys.go +++ b/x/ccv/provider/types/keys.go @@ -123,6 +123,10 @@ const ( // ConsumerAddrsToPruneBytePrefix is the byte prefix that will store the mapping from VSC ids // to consumer validators addresses needed for pruning ConsumerAddrsToPruneBytePrefix + + // SlashLogBytePrefix is the byte prefix that will store the mapping from provider address to boolean + // denoting whether the provider address has commited any double signign infractions + SlashLogBytePrefix ) // PortKey returns the key to the port ID in the store @@ -439,3 +443,8 @@ func ParseChainIdAndConsAddrKey(prefix byte, bz []byte) (string, sdk.ConsAddress addr := bz[prefixL+8+int(chainIdL):] return chainID, addr, nil } + +// SlashLogKey returns the key to a validator's slash log +func SlashLogKey(providerAddr sdk.ConsAddress) []byte { + return append([]byte{SlashAcksBytePrefix}, providerAddr.Bytes()...) +}