diff --git a/x/ccv/provider/keeper/consumer_equivocation.go b/x/ccv/provider/keeper/consumer_equivocation.go index 703c8122f2..fb9fa859ed 100644 --- a/x/ccv/provider/keeper/consumer_equivocation.go +++ b/x/ccv/provider/keeper/consumer_equivocation.go @@ -2,6 +2,7 @@ package keeper import ( "bytes" + "encoding/binary" "fmt" errorsmod "cosmossdk.io/errors" @@ -39,6 +40,18 @@ func (k Keeper) HandleConsumerDoubleVoting( ) } + // check that the evidence is not too old + minHeight := k.GetEquivocationEvidenceMinHeight(ctx, chainID) + if uint64(evidence.VoteA.Height) < minHeight { + return errorsmod.Wrapf( + ccvtypes.ErrInvalidDoubleVotingEvidence, + "evidence for consumer chain %s is too old - evidence height (%d), min (%d)", + chainID, + evidence.VoteA.Height, + minHeight, + ) + } + // verifies the double voting evidence using the consumer chain public key if err := k.VerifyDoubleVotingEvidence(*evidence, chainID, pubkey); err != nil { return err @@ -276,25 +289,20 @@ func headerToLightBlock(h ibctmtypes.Header) (*tmtypes.LightBlock, error) { // CheckMisbehaviour checks that headers in the given misbehaviour forms // a valid light client attack from an ICS consumer chain and that the light client isn't expired func (k Keeper) CheckMisbehaviour(ctx sdk.Context, misbehaviour ibctmtypes.Misbehaviour) error { + consumerChainID := misbehaviour.Header1.Header.ChainID + // check that the misbehaviour is for an ICS consumer chain - clientId, found := k.GetConsumerClientId(ctx, misbehaviour.Header1.Header.ChainID) + clientId, found := k.GetConsumerClientId(ctx, consumerChainID) if !found { - return fmt.Errorf("incorrect misbehaviour with conflicting headers from a non-existent consumer chain: %s", misbehaviour.Header1.Header.ChainID) + return fmt.Errorf("incorrect misbehaviour with conflicting headers from a non-existent consumer chain: %s", consumerChainID) } else if misbehaviour.ClientId != clientId { return fmt.Errorf("incorrect misbehaviour: expected client ID for consumer chain %s is %s got %s", - misbehaviour.Header1.Header.ChainID, + consumerChainID, clientId, misbehaviour.ClientId, ) } - clientState, found := k.clientKeeper.GetClientState(ctx, clientId) - if !found { - return sdkerrors.Wrapf(ibcclienttypes.ErrClientNotFound, "cannot check misbehaviour for client with ID %s", misbehaviour.GetClientID()) - } - - clientStore := k.clientKeeper.ClientStore(ctx, misbehaviour.GetClientID()) - // Check that the headers are at the same height to ensure that // the misbehaviour is for a light client attack and not a time violation, // see ibc-go/modules/light-clients/07-tendermint/types/misbehaviour_handle.go @@ -302,6 +310,28 @@ func (k Keeper) CheckMisbehaviour(ctx sdk.Context, misbehaviour ibctmtypes.Misbe return sdkerrors.Wrap(ibcclienttypes.ErrInvalidMisbehaviour, "headers are not at same height") } + // Check that the evidence is not too old + minHeight := k.GetEquivocationEvidenceMinHeight(ctx, consumerChainID) + evidenceHeight := misbehaviour.Header1.GetHeight().GetRevisionHeight() + // Note that the revision number is not relevant for checking the age of evidence + // as it's already part of the chain ID and the minimum height is mapped to chain IDs + if evidenceHeight < minHeight { + return errorsmod.Wrapf( + ccvtypes.ErrInvalidDoubleVotingEvidence, + "evidence for consumer chain %s is too old - evidence height (%d), min (%d)", + consumerChainID, + evidenceHeight, + minHeight, + ) + } + + clientState, found := k.clientKeeper.GetClientState(ctx, clientId) + if !found { + return sdkerrors.Wrapf(ibcclienttypes.ErrClientNotFound, "cannot check misbehaviour for client with ID %s", misbehaviour.GetClientID()) + } + + clientStore := k.clientKeeper.ClientStore(ctx, misbehaviour.GetClientID()) + // CheckMisbehaviourAndUpdateState verifies the misbehaviour against the trusted consensus states // but does NOT update the light client state. // Note that the IBC CheckMisbehaviourAndUpdateState method returns an error if the trusted consensus states are expired, @@ -444,3 +474,32 @@ func (k Keeper) SlashValidator(ctx sdk.Context, providerAddr providertypes.Provi // // CRUD section // + +// SetEquivocationEvidenceMinHeight sets the the minimum height +// of a valid consumer equivocation evidence for a given consumer chain ID +func (k Keeper) SetEquivocationEvidenceMinHeight(ctx sdk.Context, chainID string, height uint64) { + store := ctx.KVStore(k.storeKey) + heightBytes := make([]byte, 8) + binary.BigEndian.PutUint64(heightBytes, height) + + store.Set(providertypes.EquivocationEvidenceMinHeightKey(chainID), heightBytes) +} + +// GetEquivocationEvidenceMinHeight returns the the minimum height +// of a valid consumer equivocation evidence for a given consumer chain ID +func (k Keeper) GetEquivocationEvidenceMinHeight(ctx sdk.Context, chainID string) uint64 { + store := ctx.KVStore(k.storeKey) + bz := store.Get(providertypes.EquivocationEvidenceMinHeightKey(chainID)) + if bz == nil { + return 0 + } + + return binary.BigEndian.Uint64(bz) +} + +// DeleteEquivocationEvidenceMinHeight deletes the the minimum height +// of a valid consumer equivocation evidence for a given consumer chain ID +func (k Keeper) DeleteEquivocationEvidenceMinHeight(ctx sdk.Context, chainID string) { + store := ctx.KVStore(k.storeKey) + store.Delete(providertypes.EquivocationEvidenceMinHeightKey(chainID)) +} diff --git a/x/ccv/provider/types/keys.go b/x/ccv/provider/types/keys.go index 4e15f566ef..8edc655abd 100644 --- a/x/ccv/provider/types/keys.go +++ b/x/ccv/provider/types/keys.go @@ -138,6 +138,10 @@ const ( // handled in the current block VSCMaturedHandledThisBlockBytePrefix + // EquivocationEvidenceMinHeightBytePrefix is the byte prefix storing the mapping from consumer chain IDs + // to the minimum height of a valid consumer equivocation evidence + EquivocationEvidenceMinHeightBytePrefix + // NOTE: DO NOT ADD NEW BYTE PREFIXES HERE WITHOUT ADDING THEM TO getAllKeyPrefixes() IN keys_test.go ) @@ -377,6 +381,12 @@ func ConsumerRewardDenomsKey(denom string) []byte { return append([]byte{ConsumerRewardDenomsBytePrefix}, []byte(denom)...) } +// EquivocationEvidenceMinHeightKey returns the key storing the minimum height +// of a valid consumer equivocation evidence for a given consumer chain ID +func EquivocationEvidenceMinHeightKey(consumerChainID string) []byte { + return append([]byte{EquivocationEvidenceMinHeightBytePrefix}, []byte(consumerChainID)...) +} + // NOTE: DO NOT ADD FULLY DEFINED KEY FUNCTIONS WITHOUT ADDING THEM TO getAllFullyDefinedKeys() IN keys_test.go // diff --git a/x/ccv/provider/types/keys_test.go b/x/ccv/provider/types/keys_test.go index 03493c1138..e60b579b41 100644 --- a/x/ccv/provider/types/keys_test.go +++ b/x/ccv/provider/types/keys_test.go @@ -52,6 +52,7 @@ func getAllKeyPrefixes() []byte { providertypes.ConsumerAddrsToPruneBytePrefix, providertypes.SlashLogBytePrefix, providertypes.VSCMaturedHandledThisBlockBytePrefix, + providertypes.EquivocationEvidenceMinHeightBytePrefix, } } @@ -96,6 +97,7 @@ func getAllFullyDefinedKeys() [][]byte { providertypes.ConsumerAddrsToPruneKey("chainID", 88), providertypes.SlashLogKey(providertypes.NewProviderConsAddress([]byte{0x05})), providertypes.VSCMaturedHandledThisBlockKey(), + providertypes.EquivocationEvidenceMinHeightKey("chainID"), } }