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: Add Consumer chain initiated slashing #28

Merged
merged 1 commit into from
Jan 21, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions app/app.go
Original file line number Diff line number Diff line change
@@ -305,7 +305,7 @@ func New(
appCodec, keys[distrtypes.StoreKey], app.GetSubspace(distrtypes.ModuleName), app.AccountKeeper, app.BankKeeper,
&stakingKeeper, authtypes.FeeCollectorName, app.ModuleAccountAddrs(),
)
app.SlashingKeeper = slashingkeeper.NewKeeper(
slashingKeeper := slashingkeeper.NewKeeper(
appCodec, keys[slashingtypes.StoreKey], &stakingKeeper, app.GetSubspace(slashingtypes.ModuleName),
)
app.CrisisKeeper = crisiskeeper.NewKeeper(
@@ -320,7 +320,7 @@ func New(
// register the staking hooks
// NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks
app.StakingKeeper = *stakingKeeper.SetHooks(
stakingtypes.NewMultiStakingHooks(app.DistrKeeper.Hooks(), app.SlashingKeeper.Hooks(), app.ParentKeeper.Hooks()),
stakingtypes.NewMultiStakingHooks(app.DistrKeeper.Hooks(), slashingKeeper.Hooks(), app.ParentKeeper.Hooks()),
)

// Create IBC Keeper
@@ -330,11 +330,16 @@ func New(

// Create CCV child and parent keepers and modules
app.ChildKeeper = ibcchildkeeper.NewKeeper(appCodec, keys[ibcchildtypes.StoreKey], app.GetSubspace(ibcchildtypes.ModuleName), scopedIBCChildKeeper,
app.IBCKeeper.ChannelKeeper, &app.IBCKeeper.PortKeeper, app.IBCKeeper.ConnectionKeeper, app.IBCKeeper.ClientKeeper,
app.IBCKeeper.ChannelKeeper, &app.IBCKeeper.PortKeeper, app.IBCKeeper.ConnectionKeeper, app.IBCKeeper.ClientKeeper, slashingKeeper,
)

// register the slashing hooks, do it here so that
// the ChildKeeper has already been constructed
app.SlashingKeeper = *slashingKeeper.SetHooks(app.ChildKeeper.Hooks())

childModule := ibcchild.NewAppModule(app.ChildKeeper)
app.ParentKeeper = ibcparentkeeper.NewKeeper(appCodec, keys[ibcparenttypes.StoreKey], app.GetSubspace(ibcparenttypes.ModuleName), scopedIBCParentKeeper,
app.IBCKeeper.ChannelKeeper, &app.IBCKeeper.PortKeeper, app.IBCKeeper.ConnectionKeeper, app.IBCKeeper.ClientKeeper, app.StakingKeeper)
app.IBCKeeper.ChannelKeeper, &app.IBCKeeper.PortKeeper, app.IBCKeeper.ConnectionKeeper, app.IBCKeeper.ClientKeeper, app.StakingKeeper, app.SlashingKeeper)
parentModule := ibcparent.NewAppModule(&app.ParentKeeper)

// register the proposal types
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -33,6 +33,7 @@ require (

replace (
github.com/99designs/keyring => github.com/cosmos/keyring v1.1.7-0.20210622111912-ef00f8ac3d76
github.com/cosmos/cosmos-sdk => github.com/cosmos/cosmos-sdk v0.44.1-0.20220112185710-fa19ad5f85c5
github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1
google.golang.org/grpc => google.golang.org/grpc v1.33.2
)
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -177,10 +177,8 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cosmos/cosmos-sdk v0.44.0/go.mod h1:orG0jzFJ2KsDfzLd/X0JSOMzF4Oxc/BQz2GkcYF4gRE=
github.com/cosmos/cosmos-sdk v0.44.1-0.20211110111721-272089fe169b/go.mod h1:orG0jzFJ2KsDfzLd/X0JSOMzF4Oxc/BQz2GkcYF4gRE=
github.com/cosmos/cosmos-sdk v0.44.1-0.20220106232020-8ae6404991b8 h1:0N2w8v/RmMuf3YCV/GAWhNCgFuBVkiN+ggJy2cAwA6k=
github.com/cosmos/cosmos-sdk v0.44.1-0.20220106232020-8ae6404991b8/go.mod h1:orG0jzFJ2KsDfzLd/X0JSOMzF4Oxc/BQz2GkcYF4gRE=
github.com/cosmos/cosmos-sdk v0.44.1-0.20220112185710-fa19ad5f85c5 h1:LpMtX4wYDk1oYTiPjq80h9WGhV18L+2TOnF/B0AiH/U=
github.com/cosmos/cosmos-sdk v0.44.1-0.20220112185710-fa19ad5f85c5/go.mod h1:orG0jzFJ2KsDfzLd/X0JSOMzF4Oxc/BQz2GkcYF4gRE=
github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y=
github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY=
github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw=
12 changes: 12 additions & 0 deletions proto/interchain_security/ccv/v1/ccv.proto
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ option go_package = "github.com/cosmos/interchain-security/x/ccv/types";
import "gogoproto/gogo.proto";
import "tendermint/abci/types.proto";


// This packet is sent from parent chain to baby chain if the validator set for baby chain
// changes (due to new bonding/unbonding messages or slashing events)
// The acknowledgement from baby chain will be sent asynchronously once unbonding period is over,
@@ -38,3 +39,14 @@ message UnbondingDelegationEntry {
// child chains that are still unbonding
repeated string unbonding_child_chains = 3;
}

// This packet is sent from the consumer chain to the provider chain.
// The acknowledgement will be sent asynchrounously when the jailing
// will be started on the provider chain.
message ValidatorDowntimePacketData {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you creating separate packet data type for each type of evidence?

My understanding of v1, was that we just send back a generic evidence packet with validator and slash fraction

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that Simon is generalizing this in his current work

tendermint.abci.Validator validator = 1
[(gogoproto.nullable) = false, (gogoproto.moretags) = "yaml:\"validator\""];
int64 jail_time = 2;
int64 slash_fraction = 3;
uint64 valset_update_id = 4;
}
59 changes: 59 additions & 0 deletions x/ccv/child/keeper/hooks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package keeper

import (
sdk "github.com/cosmos/cosmos-sdk/types"
slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types"
abci "github.com/tendermint/tendermint/abci/types"
)

var _ slashingtypes.SlashingHooks = Keeper{}

// AfterValidatorDowntime gets the given validator address jailing time
// in order either to add it the downtime jailing duration and initiated its slashing
// on the provider chain or perfom a no-op
func (k Keeper) AfterValidatorDowntime(ctx sdk.Context, consAddr sdk.ConsAddress, power int64) {
// get validator signing info
signInfo, _ := k.slashingKeeper.GetValidatorSigningInfo(ctx, consAddr)

// do nothing if validator is jailed
if ctx.BlockTime().UnixNano() < int64(signInfo.JailedUntil.UnixNano()) {
return
}

// increase jail time
signInfo.JailedUntil = ctx.BlockHeader().Time.Add(k.slashingKeeper.DowntimeJailDuration(ctx))

// reset the missed block counters
signInfo.MissedBlocksCounter = 0
signInfo.IndexOffset = 0
k.slashingKeeper.ClearValidatorMissedBlockBitArray(ctx, consAddr)

// update signing info
k.slashingKeeper.SetValidatorSigningInfo(ctx, consAddr, signInfo)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this design will lead to confusion and bugs. Basically, AfterValidatorDowntime() is a hook called by the slashing module in order to allow other modules (that register this hook) to execute some code. This code though is changing the state of the slashing module.

My suggestion would be to leave only the call to SendPacket() here, and add all the other logic in the slashing module.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s a matter of opinion I guess. By adding the logic into the slashing module, it would introduce operations specific to consumer chains and not provider chains. I believe we prefer to have this kind of distinction implemented in the CCV module only.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I guess this approach make sense for V1 where we try to keep the changes to the rest of the SDK to the minimum. The alternative would be to have a global flag on the consumer chain indicating that the chain is a consumer chain, and add specific logic for that, but I agree, we don't need it for V1.

BTW, in the slashing module we should make sure that this line doesn't actually return a validator (the staking module on the consumer chain may have validators with the same keys as the ones coming from the provider). @jtremback this could be possible, right?


// send packet to initiate slashing on the provider chain
k.SendPacket(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This send packet API is confusing. See the SendPacket api in parent keeper or transfer keeper.

I think this should be something like `SendPacket(ctx, packet)

ctx,
abci.Validator{
Address: consAddr.Bytes(),
Power: power,
},
k.slashingKeeper.SlashFractionDowntime(ctx).TruncateInt64(),
signInfo.JailedUntil.UnixNano(),
)
}

// Hooks wrapper struct for ChildKeeper
type Hooks struct {
k Keeper
}

// Return the wrapper struct
func (k Keeper) Hooks() Hooks {
return Hooks{k}
}

// AfterValidatorDowntime implements the slashing hook interface
func (h Hooks) AfterValidatorDowntime(ctx sdk.Context, consAddr sdk.ConsAddress, power int64) {
h.k.AfterValidatorDowntime(ctx, consAddr, power)
}
17 changes: 16 additions & 1 deletion x/ccv/child/keeper/keeper.go
Original file line number Diff line number Diff line change
@@ -30,13 +30,14 @@ type Keeper struct {
portKeeper ccv.PortKeeper
connectionKeeper ccv.ConnectionKeeper
clientKeeper ccv.ClientKeeper
slashingKeeper ccv.SlashingKeeper
}

// NewKeeper creates a new Child Keeper instance
func NewKeeper(
cdc codec.BinaryCodec, key sdk.StoreKey, paramSpace paramtypes.Subspace, scopedKeeper capabilitykeeper.ScopedKeeper,
channelKeeper ccv.ChannelKeeper, portKeeper ccv.PortKeeper,
connectionKeeper ccv.ConnectionKeeper, clientKeeper ccv.ClientKeeper,
connectionKeeper ccv.ConnectionKeeper, clientKeeper ccv.ClientKeeper, slashingKeeper ccv.SlashingKeeper,
) Keeper {
// set KeyTable if it has not already been set
if !paramSpace.HasKeyTable() {
@@ -52,6 +53,7 @@ func NewKeeper(
portKeeper: portKeeper,
connectionKeeper: connectionKeeper,
clientKeeper: clientKeeper,
slashingKeeper: slashingKeeper,
}
}

@@ -314,3 +316,16 @@ func (k Keeper) VerifyParentChain(ctx sdk.Context, channelID string) error {

return nil
}

// GetLastUnbondingPacket returns the last unbounding packet stored in lexical order
func (k Keeper) GetLastUnbondingPacket(ctx sdk.Context) ccv.ValidatorSetChangePacketData {
ubdPacket := &channeltypes.Packet{}
k.IterateUnbondingPacket(ctx, func(seq uint64, packet channeltypes.Packet) bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be very inefficient. Since the unbonding period is ~4weeks, the number of unbonding packets stored at any given time is ~345600 (given a validator set change packet every block, and a block every 7 seconds). Now that I think about it, I don't think it's a good idea to even store so many packets :)

Nonetheless, here I suggest keeping a state w/ the ValsetUpdateId of the latest received packet.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to iterate in reverse order and just return on first element. See store iteration methods

*ubdPacket = packet
return false
})
var data ccv.ValidatorSetChangePacketData

ccv.ModuleCdc.UnmarshalJSON(ubdPacket.GetData(), &data)
return data
}
118 changes: 115 additions & 3 deletions x/ccv/child/keeper/keeper_test.go
Original file line number Diff line number Diff line change
@@ -3,24 +3,27 @@ package keeper_test
import (
"fmt"
"testing"

ibctesting "github.com/cosmos/ibc-go/testing"
"github.com/stretchr/testify/suite"
"time"

cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
sdk "github.com/cosmos/cosmos-sdk/types"
slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types"
clienttypes "github.com/cosmos/ibc-go/modules/core/02-client/types"
channeltypes "github.com/cosmos/ibc-go/modules/core/04-channel/types"
commitmenttypes "github.com/cosmos/ibc-go/modules/core/23-commitment/types"
ibctmtypes "github.com/cosmos/ibc-go/modules/light-clients/07-tendermint/types"
ibctesting "github.com/cosmos/ibc-go/testing"
"github.com/cosmos/interchain-security/app"
"github.com/cosmos/interchain-security/testutil/simapp"
"github.com/cosmos/interchain-security/x/ccv/child/types"
childtypes "github.com/cosmos/interchain-security/x/ccv/child/types"
parenttypes "github.com/cosmos/interchain-security/x/ccv/parent/types"
ccv "github.com/cosmos/interchain-security/x/ccv/types"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
// slashing "github.com/cosmos/cosmos-sdk/x/slashing"
// staking "github.com/cosmos/cosmos-sdk/x/staking"
)

func init() {
@@ -361,3 +364,112 @@ func (suite *KeeperTestSuite) TestVerifyParentChain() {
func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(KeeperTestSuite))
}

// TestValidatorDowntime tests that the slashing hooks
// are registred and triggered when a validator has downtime
func (suite *KeeperTestSuite) TestValidatorDowntime() {
// initial setup
app := suite.childChain.App.(*app.App)
ctx := suite.childChain.GetContext()

// create a validator pubkey and address
pubkey := ed25519.GenPrivKey().PubKey()
consAddr := sdk.ConsAddress(pubkey.Address())

// add the validator pubkey and signing info to the store
app.SlashingKeeper.AddPubkey(ctx, pubkey)

valInfo := slashingtypes.NewValidatorSigningInfo(consAddr, ctx.BlockHeight(), ctx.BlockHeight()-1,
time.Time{}.UTC(), false, int64(0))
app.SlashingKeeper.SetValidatorSigningInfo(ctx, consAddr, valInfo)

// Sign 1000 blocks
valPower := int64(1)
height := int64(0)
for ; height < app.SlashingKeeper.SignedBlocksWindow(ctx); height++ {
ctx = ctx.WithBlockHeight(height)
app.SlashingKeeper.HandleValidatorSignature(ctx, pubkey.Address().Bytes(), valPower, true)
}
// Miss 500 blocks
for ; height < app.SlashingKeeper.SignedBlocksWindow(ctx)+(app.SlashingKeeper.SignedBlocksWindow(ctx)-app.SlashingKeeper.MinSignedPerWindow(ctx))+1; height++ {
ctx = ctx.WithBlockHeight(height)
app.SlashingKeeper.HandleValidatorSignature(ctx, pubkey.Address().Bytes(), valPower, false)
}

signInfo, found := app.SlashingKeeper.GetValidatorSigningInfo(ctx, consAddr)
suite.Require().True(found)
suite.Require().NotZero(signInfo.JailedUntil)
suite.Require().Zero(signInfo.MissedBlocksCounter)
suite.Require().Zero(signInfo.IndexOffset)
app.SlashingKeeper.IterateValidatorMissedBlockBitArray(ctx, consAddr, func(_ int64, missed bool) bool {
suite.Require().False(missed)
return false
Comment on lines +399 to +406
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests should check that the slashing packet is actually sent

})
}

// TestAfterValidatorDowntimeHook tests the slashing hook implementation logic
func (suite *KeeperTestSuite) TestAfterValidatorDowntimeHook() {
app := suite.childChain.App.(*app.App)
ctx := suite.ctx

consAddr := sdk.ConsAddress(ed25519.GenPrivKey().PubKey().Bytes()).Bytes()

now := time.Now()

// Cover the cases when the validatir jailing duration
// is either elapsed, null or still going
testcases := []struct {
jailedUntil time.Time
expUpdate bool
}{{
jailedUntil: now.Add(-1 * time.Hour),
expUpdate: true,
}, {
jailedUntil: time.Time{}, // null
expUpdate: true,
}, {
jailedUntil: now.Add(1 * time.Hour),
expUpdate: false,
},
}

// synchronize the block time with the test cases
ctx = ctx.WithBlockTime(now)
valInfo := slashingtypes.NewValidatorSigningInfo(consAddr, int64(1), int64(1),
time.Time{}.UTC(), false, int64(0))

for _, tc := range testcases {
// set the current unjailing time
valInfo.JailedUntil = tc.jailedUntil
app.SlashingKeeper.SetValidatorSigningInfo(ctx, consAddr, valInfo)
// execute the hook logic
app.ChildKeeper.AfterValidatorDowntime(
ctx, consAddr, int64(1))
// verify if we have the expected output
signInfo, found := app.SlashingKeeper.GetValidatorSigningInfo(ctx, consAddr)
suite.Require().True(found)
suite.Require().True(tc.expUpdate == !(signInfo.JailedUntil.Equal(tc.jailedUntil)))
Comment on lines +449 to +451
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

}

suite.Require()
}

func (suite *KeeperTestSuite) TestGetLastUnboundingPacket() {
app := suite.childChain.App.(*app.App)
ctx := suite.childChain.GetContext()

ubdPacket := app.ChildKeeper.GetLastUnbondingPacket(ctx)
suite.Require().Zero(ubdPacket.ValsetUpdateId)
for i := 0; i < 5; i++ {
pd := ccv.NewValidatorSetChangePacketData(
[]abci.ValidatorUpdate{},
uint64(i),
)
packet := channeltypes.NewPacket(pd.GetBytes(), uint64(i), "", "", "", "",
clienttypes.NewHeight(1, 0), 0)
app.ChildKeeper.SetUnbondingPacket(ctx, uint64(i), packet)
}

ubdPacket = app.ChildKeeper.GetLastUnbondingPacket(ctx)
suite.Require().Equal(uint64(4), ubdPacket.ValsetUpdateId)
}
55 changes: 55 additions & 0 deletions x/ccv/child/keeper/relay.go
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import (

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
clienttypes "github.com/cosmos/ibc-go/modules/core/02-client/types"
channeltypes "github.com/cosmos/ibc-go/modules/core/04-channel/types"
host "github.com/cosmos/ibc-go/modules/core/24-host"
"github.com/cosmos/ibc-go/modules/core/exported"
@@ -88,3 +89,57 @@ func (k Keeper) UnbondMaturePackets(ctx sdk.Context) error {
}
return nil
}

// SendPacket sends a packet that initiates the given validator
// slashing and jailing on the provider chain.
func (k Keeper) SendPacket(ctx sdk.Context, val abci.Validator, slashFraction, jailedUntil int64) error {

// check the setup
channelID, ok := k.GetParentChannel(ctx)
if !ok {
return sdkerrors.Wrap(channeltypes.ErrChannelNotFound, "parent channel not set")
}
channel, ok := k.channelKeeper.GetChannel(ctx, types.PortID, channelID)
if !ok {
return sdkerrors.Wrapf(channeltypes.ErrChannelNotFound, "channel not found for channel ID: %s", channelID)
}
channelCap, ok := k.scopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(types.PortID, channelID))
if !ok {
return sdkerrors.Wrap(channeltypes.ErrChannelCapabilityNotFound, "module does not own channel capability")
}

// get the next sequence
sequence, found := k.channelKeeper.GetNextSequenceSend(ctx, types.PortID, channelID)
if !found {
return sdkerrors.Wrapf(
channeltypes.ErrSequenceSendNotFound,
"source port: %s, source channel: %s", types.PortID, channelID,
)
}

// add the last ValsetUpdateId to the packet data so that the provider
// can find the block height when the downtime happened
valsetUpdateId := k.GetLastUnbondingPacket(ctx).ValsetUpdateId
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we don't really need the packet, only the ValsetUpdateId. See the comment on GetLastUnbondingPacket().

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if this is the only packet sent from provider chain. This should be the same as NextSeqRecv and we don't need the ValsetUpdateId in the packet data. We should discuss when you get back.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ValsetUpdateId is a provider variable that is used for all consumer chains; the NextSeqRecv will be one per consumer chain channel. In addition, I think using ValsetUpdateId is cleaner and avoids potential issues if we decide to add more packets sent by the provider.

if valsetUpdateId == 0 {
return sdkerrors.Wrapf(ccv.ErrInvalidChildState, "last valset update id not set")
}
packetData := ccv.NewValidatorDowtimePacketData(val, valsetUpdateId, slashFraction, jailedUntil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spelling error

packetDataBytes := packetData.GetBytes()

// send ValidatorDowntime infractions in IBC packet
packet := channeltypes.NewPacket(
packetDataBytes, sequence,
types.PortID, channelID,
channel.Counterparty.PortId, channel.Counterparty.ChannelId,
clienttypes.Height{}, uint64(ccv.GetTimeoutTimestamp(ctx.BlockTime()).UnixNano()),
)
if err := k.channelKeeper.SendPacket(ctx, channelCap, packet); err != nil {
return err
}

return nil
}

func (k Keeper) OnAcknowledgementPacket(ctx sdk.Context, packet channeltypes.Packet, ack channeltypes.Acknowledgement) error {
return nil
}
7 changes: 6 additions & 1 deletion x/ccv/child/module.go
Original file line number Diff line number Diff line change
@@ -351,10 +351,15 @@ func (am AppModule) OnRecvPacket(
// OnAcknowledgementPacket implements the IBCModule interface
func (am AppModule) OnAcknowledgementPacket(
ctx sdk.Context,
packet channeltypes.Packet,
_ channeltypes.Packet,
acknowledgement []byte,
_ sdk.AccAddress,
) (*sdk.Result, error) {
var ack channeltypes.Acknowledgement
if err := ccv.ModuleCdc.UnmarshalJSON(acknowledgement, &ack); err != nil {
fmt.Println(err)
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "cannot unmarshal parent packet acknowledgement: %v", err)
}
return nil, sdkerrors.Wrap(ccv.ErrInvalidChannelFlow, "cannot receive acknowledgement on child port, child chain does not send packet over channel.")
}
Comment on lines 352 to 364
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (am AppModule) OnAcknowledgementPacket(
ctx sdk.Context,
packet channeltypes.Packet,
_ channeltypes.Packet,
acknowledgement []byte,
_ sdk.AccAddress,
) (*sdk.Result, error) {
var ack channeltypes.Acknowledgement
if err := ccv.ModuleCdc.UnmarshalJSON(acknowledgement, &ack); err != nil {
fmt.Println(err)
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "cannot unmarshal parent packet acknowledgement: %v", err)
}
return nil, sdkerrors.Wrap(ccv.ErrInvalidChannelFlow, "cannot receive acknowledgement on child port, child chain does not send packet over channel.")
}
func (am AppModule) OnAcknowledgementPacket(
ctx sdk.Context,
packet channeltypes.Packet,
acknowledgement []byte,
_ sdk.AccAddress,
) (*sdk.Result, error) {
var ack channeltypes.Acknowledgement
if err := ccv.ModuleCdc.UnmarshalJSON(acknowledgement, &ack); err != nil {
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "cannot unmarshal child packet acknowledgement: %v", err)
}
var data ccv.ValidatorDowntimePacketData
if err := ccv.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil {
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "cannot unmarshal child packet data: %s", err.Error())
}
if err := am.keeper.OnAcknowledgementPacket(ctx, packet, data, ack); err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
ccv.EventTypePacket,
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
sdk.NewAttribute(ccv.AttributeKeyAck, ack.String()),
),
)
switch resp := ack.Response.(type) {
case *channeltypes.Acknowledgement_Result:
ctx.EventManager().EmitEvent(
sdk.NewEvent(
ccv.EventTypePacket,
sdk.NewAttribute(ccv.AttributeKeyAckSuccess, string(resp.Result)),
),
)
case *channeltypes.Acknowledgement_Error:
ctx.EventManager().EmitEvent(
sdk.NewEvent(
ccv.EventTypePacket,
sdk.NewAttribute(ccv.AttributeKeyAckError, resp.Error),
),
)
}
return &sdk.Result{
Events: ctx.EventManager().Events().ToABCIEvents(),
}, nil
}

Currently, any ACK from the provider chain will results in error.


29 changes: 27 additions & 2 deletions x/ccv/parent/keeper/keeper.go
Original file line number Diff line number Diff line change
@@ -37,14 +37,14 @@ type Keeper struct {
connectionKeeper ccv.ConnectionKeeper
clientKeeper ccv.ClientKeeper
stakingKeeper ccv.StakingKeeper
slashingKeeper ccv.SlashingKeeper
}

// NewKeeper creates a new parent Keeper instance
func NewKeeper(
cdc codec.BinaryCodec, key sdk.StoreKey, paramSpace paramtypes.Subspace, scopedKeeper capabilitykeeper.ScopedKeeper,
channelKeeper ccv.ChannelKeeper, portKeeper ccv.PortKeeper,
connectionKeeper ccv.ConnectionKeeper, clientKeeper ccv.ClientKeeper,
stakingKeeper ccv.StakingKeeper,
connectionKeeper ccv.ConnectionKeeper, clientKeeper ccv.ClientKeeper, stakingKeeper ccv.StakingKeeper, slashingKeeper ccv.SlashingKeeper,
) Keeper {
// set KeyTable if it has not already been set
if !paramSpace.HasKeyTable() {
@@ -61,6 +61,7 @@ func NewKeeper(
connectionKeeper: connectionKeeper,
clientKeeper: clientKeeper,
stakingKeeper: stakingKeeper,
slashingKeeper: slashingKeeper,
}
}

@@ -447,3 +448,27 @@ func (h StakingHooks) BeforeUnbondingDelegationEntryComplete(ctx sdk.Context, ID

return found
}

// SetValsetUpdateBlockHeight sets the block height for a given valset update id
func (k Keeper) SetValsetUpdateBlockHeight(ctx sdk.Context, valsetUpdateId, blockHeight uint64) {
store := ctx.KVStore(k.storeKey)
heightBytes := make([]byte, 8)
binary.BigEndian.PutUint64(heightBytes, blockHeight)
store.Set(types.ValsetUpdateBlockHeightKey(valsetUpdateId), heightBytes)
}

// GetValsetUpdateBlockHeight gets the block height for a given valset update id
func (k Keeper) GetValsetUpdateBlockHeight(ctx sdk.Context, valsetUpdateId uint64) uint64 {
store := ctx.KVStore(k.storeKey)
bz := store.Get(types.ValsetUpdateBlockHeightKey(valsetUpdateId))
if bz == nil {
return 0
}
return binary.BigEndian.Uint64(bz)
}

// DeleteValsetUpdateBlockHeight deletes the block height value for a given vaset update id
func (k Keeper) DeleteValsetUpdateBlockHeight(ctx sdk.Context, valsetUpdateId uint64) {
store := ctx.KVStore(k.storeKey)
store.Delete(types.ValsetUpdateBlockHeightKey(valsetUpdateId))
}
42 changes: 42 additions & 0 deletions x/ccv/parent/keeper/keeper_test.go
Original file line number Diff line number Diff line change
@@ -88,3 +88,45 @@ func (suite *KeeperTestSuite) SetupTest() {
func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(KeeperTestSuite))
}

func (suite *KeeperTestSuite) TestValsetUpdateBlockHeight() {
app := suite.parentChain.App.(*app.App)
ctx := suite.ctx

blockHeight := app.ParentKeeper.GetValsetUpdateBlockHeight(ctx, uint64(0))
suite.Require().Zero(blockHeight)

app.ParentKeeper.SetValsetUpdateBlockHeight(ctx, uint64(1), uint64(2))
blockHeight = app.ParentKeeper.GetValsetUpdateBlockHeight(ctx, uint64(1))
suite.Require().Equal(blockHeight, uint64(2))

app.ParentKeeper.DeleteValsetUpdateBlockHeight(ctx, uint64(1))
blockHeight = app.ParentKeeper.GetValsetUpdateBlockHeight(ctx, uint64(1))
suite.Require().Zero(blockHeight)

app.ParentKeeper.SetValsetUpdateBlockHeight(ctx, uint64(1), uint64(2))
app.ParentKeeper.SetValsetUpdateBlockHeight(ctx, uint64(3), uint64(4))
blockHeight = app.ParentKeeper.GetValsetUpdateBlockHeight(ctx, uint64(3))
suite.Require().Equal(blockHeight, uint64(4))
}

// func (suite KeeperTestSuite) TestOnRecvPacket() {
// ctx, t := suite.ctx, suite.T()

// // Get a validator

// // app := suite.parentChain.App.(*app.App)
// valset := suite.parentChain.Vals
// val, err := valset.Validators[0].ToProto()
// suite.Require().NoError(err)

// tests := []struct {
// pubkey crypto.PubKey
// jailTime int64
// }{{
// pubkey: val.PubKey,
// jailTime: int64(0),
// }}
// app.ParentKeeper.
// t.Logf("%+v", val.PubKey)
// }
57 changes: 56 additions & 1 deletion x/ccv/parent/keeper/relay.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package keeper

import (
"fmt"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
clienttypes "github.com/cosmos/ibc-go/modules/core/02-client/types"
channeltypes "github.com/cosmos/ibc-go/modules/core/04-channel/types"
host "github.com/cosmos/ibc-go/modules/core/24-host"
"github.com/cosmos/ibc-go/modules/core/exported"
"github.com/cosmos/interchain-security/x/ccv/parent/types"
ccv "github.com/cosmos/interchain-security/x/ccv/types"
abci "github.com/tendermint/tendermint/abci/types"
@@ -23,6 +27,7 @@ func (k Keeper) SendPacket(ctx sdk.Context, chainID string, valUpdates []abci.Va
if !ok {
return sdkerrors.Wrapf(channeltypes.ErrChannelNotFound, "channel not found for channel ID: %s", channelID)
}

channelCap, ok := k.scopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(types.PortID, channelID))
if !ok {
return sdkerrors.Wrap(channeltypes.ErrChannelCapabilityNotFound, "module does not own channel capability")
@@ -42,7 +47,7 @@ func (k Keeper) SendPacket(ctx sdk.Context, chainID string, valUpdates []abci.Va
packetDataBytes, sequence,
types.PortID, channelID,
channel.Counterparty.PortId, channel.Counterparty.ChannelId,
clienttypes.Height{}, uint64(types.GetTimeoutTimestamp(ctx.BlockTime()).UnixNano()),
clienttypes.Height{}, uint64(ccv.GetTimeoutTimestamp(ctx.BlockTime()).UnixNano()),
)

if err := k.channelKeeper.SendPacket(ctx, channelCap, packet); err != nil {
@@ -113,7 +118,57 @@ func (k Keeper) EndBlockCallback(ctx sdk.Context) {
if len(valUpdates) != 0 {
k.SendPacket(ctx, chainID, valUpdates, valUpdateID)
k.IncrementValidatorSetUpdateId(ctx) // TODO: this needs to be moved out of this scope
k.SetValsetUpdateBlockHeight(ctx, valUpdateID, uint64(ctx.BlockHeight()))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line (together with the line above) should be move after this scope (i.e., after the iteration over all consumer chains). Most likely they should also be reverted, i.e., the current block height should be mapped to the ValidatorSetUpdateId of the packet that was just sent.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line (together with the line above) should be move after this scope (i.e., after the iteration over all consumer chains)

Why?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line (together with the line above) should be move after this scope (i.e., after the iteration over all consumer chains)

Why?

All the undelegations happening during this block are mapped with the current ValidatorSetUpdateId (or in general, to the next ValidatorSetUpdate packet). If we increment ValidatorSetUpdateId before the loop, then the undelegations will be mapped to the previously sent packet.

}
return false
})
}

// OnRecvPacket slahes and jails the given validator in the packet data
func (k Keeper) OnRecvPacket(ctx sdk.Context, packet channeltypes.Packet, data ccv.ValidatorDowntimePacketData) exported.Acknowledgement {
if childChannel, ok := k.GetChannelToChain(ctx, packet.DestinationChannel); !ok && childChannel != packet.DestinationChannel {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty confusing to read, maybe break it onto several lines with named variable

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AdityaSripal we are having a hard time understanding this boilerplate. This seems incorrect.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually maybe the issue is that @sainoe accidently ihnverted the ok (!ok)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetChannelToChain returns the chain ID associated to the packet.DestinationChannel. You shouldn't compare the result to a channel ID.

ack := channeltypes.NewErrorAcknowledgement(
fmt.Sprintf("packet sent on a channel %s other than the established child channel %s", packet.DestinationChannel, childChannel),
)
chanCap, _ := k.scopedKeeper.GetCapability(ctx, host.ChannelCapabilityPath(packet.DestinationPort, packet.DestinationChannel))
k.channelKeeper.ChanCloseInit(ctx, packet.DestinationPort, packet.DestinationChannel, chanCap)
return &ack
}

// initiate slashing and jailing
if err := k.HandleConsumerDowntime(ctx, data); err != nil {
ack := channeltypes.NewErrorAcknowledgement(err.Error())
return &ack
}

return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return nil
// send back acknowledgement
ack := channeltypes.NewResultAcknowledgement([]byte{byte(1)})
return ack

Here we should return an ACK.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it mandatory to send an ack? It seems like it can't be because we don't send an ack for 2 weeks in the core protocol, and around 345k packets could be received without an ack coming back.

Sending it just means that we have more boilerplate on the other side.

Copy link
Contributor

@mpoke mpoke Jan 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From ICS 4:

This is an asynchronous acknowledgement, the contents of which do not need to be determined when the packet is received, only when processing is complete.

This is why the consumer can receive around 345k packets w/o ACKs. However, no ACKs can be skipped (see below).

Acknowledging packets is not required; however, if an ordered channel uses acknowledgements, either all or no packets must be acknowledged (since the acknowledgements are processed in order).

I guess the meaning here is a uni-directional channel, right? I think the packets sent by the consumer do not need to be ACKed.

Note that if packets are not acknowledged, packet commitments cannot be deleted on the source chain. Future versions of IBC may include ways for modules to specify whether or not they will be acknowledging packets in order to allow for cleanup.

However, to enable cleanup, we may need to send ACKs for all packets.

}

// HandleConsumerDowntime gets the validator and the downtime infraction height from the packet data
// validator address and valset upate ID. Then it executes the slashing the and jailing accordingly.
func (k Keeper) HandleConsumerDowntime(ctx sdk.Context, downtimeData ccv.ValidatorDowntimePacketData) error {
// get the validator consensus address
consAddr := sdk.ConsAddress(downtimeData.Validator.Address)

// get the validator data
val, found := k.stakingKeeper.GetValidatorByConsAddr(ctx, consAddr)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure that a validator from the consumer chain has the same consensus address as a validator from the provider chain?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Key Delegation functionality is WIP.

if !found {
return fmt.Errorf("cannot find validator with address %s", consAddr.String())
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// make sure the validator is not yet unbonded;
// stakingKeeper.Slash() panics otherwise
if val.IsUnbonded() {
return fmt.Errorf("validator with address %s is already unbonded", consAddr.String())
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sainoe Patch this suggestion back into main

// get the downtime block height from the valsetUpdateID
blockHeight := k.GetValsetUpdateBlockHeight(ctx, downtimeData.ValsetUpdateId)
if blockHeight == 0 {
return fmt.Errorf("cannot find validator update id %d", downtimeData.ValsetUpdateId)
}

// slash and jail the validator
k.stakingKeeper.Slash(ctx, consAddr, int64(blockHeight), downtimeData.Validator.Power, sdk.NewDec(1).QuoInt64(downtimeData.SlashFraction))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to allow the consumer chain to set the SlashFraction? We could instead use the value from the provider slashing module.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we do. It is assumed that the provider and consumer chains should agreed on these parameters off-chain during the proposal.

Copy link
Contributor

@mpoke mpoke Jan 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the consumer change these values afterwards w/o asking the provider? That's something that we should avoid. This is related to one of the open issues described here.

if !val.Jailed {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if !val.Jailed {
if !val.IsJailed() {

k.stakingKeeper.Jail(ctx, consAddr)
}
// TODO: check if the missed block bits and sign info need to be reseted
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't reset if we want a validator to be also slashed for downtime on the provider, which I think we want. A validator can already be slashed multiple times for downtime on different consumer chains.

k.slashingKeeper.JailUntil(ctx, consAddr, ctx.BlockHeader().Time.Add(time.Duration(downtimeData.JailTime)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as with the SlashFraction, do we want the consumer chain to set the JailTime?


return nil
}
24 changes: 23 additions & 1 deletion x/ccv/parent/module.go
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ import (
channeltypes "github.com/cosmos/ibc-go/modules/core/04-channel/types"
porttypes "github.com/cosmos/ibc-go/modules/core/05-port/types"
host "github.com/cosmos/ibc-go/modules/core/24-host"
"github.com/cosmos/ibc-go/modules/core/exported"
ibcexported "github.com/cosmos/ibc-go/modules/core/exported"
"github.com/cosmos/interchain-security/x/ccv/parent/keeper"
"github.com/cosmos/interchain-security/x/ccv/parent/types"
@@ -320,7 +321,28 @@ func (am AppModule) OnRecvPacket(
packet channeltypes.Packet,
_ sdk.AccAddress,
) ibcexported.Acknowledgement {
return channeltypes.NewErrorAcknowledgement("cannot receive packet on parent chain")
var (
ack exported.Acknowledgement
data ccv.ValidatorDowntimePacketData
)
if err := ccv.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil {
errAck := channeltypes.NewErrorAcknowledgement(fmt.Sprintf("cannot unmarshal CCV packet data: %s", err.Error()))
ack = &errAck
} else {
ack = am.keeper.OnRecvPacket(ctx, packet, data)
}

ctx.EventManager().EmitEvent(
sdk.NewEvent(
ccv.EventTypePacket,
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
sdk.NewAttribute(ccv.AttributeKeyAckSuccess, fmt.Sprintf("%t", ack != nil)),
),
)

// NOTE: In successful case, acknowledgement will be written asynchronously
// after unbonding period has elapsed.
Comment on lines +343 to +344
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// NOTE: In successful case, acknowledgement will be written asynchronously
// after unbonding period has elapsed.

This is not true. It's probably from the consumer chain. :)

return ack
}

// OnAcknowledgementPacket implements the IBCModule interface
170 changes: 168 additions & 2 deletions x/ccv/parent/parent_test.go
Original file line number Diff line number Diff line change
@@ -6,13 +6,18 @@ import (

sdk "github.com/cosmos/cosmos-sdk/types"

cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"

slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"

clienttypes "github.com/cosmos/ibc-go/modules/core/02-client/types"
channeltypes "github.com/cosmos/ibc-go/modules/core/04-channel/types"
commitmenttypes "github.com/cosmos/ibc-go/modules/core/23-commitment/types"
ibctmtypes "github.com/cosmos/ibc-go/modules/light-clients/07-tendermint/types"
ibctesting "github.com/cosmos/ibc-go/testing"
ccv "github.com/cosmos/interchain-security/x/ccv/types"

"github.com/cosmos/interchain-security/app"
"github.com/cosmos/interchain-security/testutil/simapp"
@@ -21,6 +26,7 @@ import (
"github.com/cosmos/interchain-security/x/ccv/types"

abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/libs/bytes"

"github.com/stretchr/testify/suite"
)
@@ -134,7 +140,7 @@ func (s *ParentTestSuite) TestPacketRoundtrip() {

// Reconstruct packet
packetData := types.NewValidatorSetChangePacketData(valUpdates, 1)
timeout := uint64(parenttypes.GetTimeoutTimestamp(oldBlockTime).UnixNano())
timeout := uint64(ccv.GetTimeoutTimestamp(oldBlockTime).UnixNano())
packet := channeltypes.NewPacket(packetData.GetBytes(), 1, parenttypes.PortID, s.path.EndpointB.ChannelID,
childtypes.PortID, s.path.EndpointA.ChannelID, clienttypes.Height{}, timeout)

@@ -214,7 +220,7 @@ func (s *ParentTestSuite) TestStakingHooks() {

sendValUpdatePacket := func(valUpdates []abci.ValidatorUpdate, valUpdateId uint64, blockTime time.Time, packetSequence uint64) channeltypes.Packet {
packetData := types.NewValidatorSetChangePacketData(valUpdates, valUpdateId)
timeout := uint64(parenttypes.GetTimeoutTimestamp(blockTime).UnixNano())
timeout := uint64(ccv.GetTimeoutTimestamp(blockTime).UnixNano())
packet := channeltypes.NewPacket(packetData.GetBytes(), packetSequence, parenttypes.PortID, s.path.EndpointB.ChannelID,
childtypes.PortID, s.path.EndpointA.ChannelID, clienttypes.Height{}, timeout)

@@ -328,3 +334,163 @@ func GetStakingUbde(ctx sdk.Context, k stakingkeeper.Keeper, id uint64) (staking

return stakingUbde, found
}

// TestSendDowntimePacket tests to the consumer initiated slashing and jailing
// on the provider chain by the consumer chain
func (s *ParentTestSuite) TestSendDowntimePacket() {
// initial setup
s.SetupCCVChannel()
parentStakingKeeper := s.parentChain.App.GetStakingKeeper()
parentSlashingKeeper := s.parentChain.App.(*app.App).SlashingKeeper

// get a parent chain validator address and balance
tmVal := s.parentChain.Vals.Validators[0]
val, err := s.parentChain.Vals.Validators[0].ToProto()
s.Require().NoError(err)
pubkey, err := cryptocodec.FromTmProtoPublicKey(val.GetPubKey())
s.Require().Nil(err)
consAddr := sdk.GetConsAddress(pubkey)
valData, found := parentStakingKeeper.GetValidatorByConsAddr(s.parentCtx(), consAddr)
s.Require().True(found)
valOldBalance := valData.Tokens

// create the validator's signing info record to allow jailing
valInfo := slashingtypes.NewValidatorSigningInfo(consAddr, s.parentCtx().BlockHeight(),
s.parentCtx().BlockHeight()-1, time.Time{}.UTC(), false, int64(0))
parentSlashingKeeper.SetValidatorSigningInfo(s.parentCtx(), consAddr, valInfo)

// create a valseUpdateId that allows to retrieve the infraction block height on the provider
valsetUpdateId := uint64(1)
// save the current block height for the last valsetUpdateId
s.parentChain.App.(*app.App).ParentKeeper.SetValsetUpdateBlockHeight(s.parentCtx(), valsetUpdateId,
uint64(s.parentCtx().BlockHeight()))
validator := abci.Validator{
Address: tmVal.Address,
Power: int64(1),
}

// construct the downtime packet with the validator address and power along
// with the slashing and jailing parameters
oldBlockTime := s.childCtx().BlockTime()
slashFraction := int64(100)
packetData := types.NewValidatorDowtimePacketData(validator, valsetUpdateId, slashFraction,
int64(slashingtypes.DefaultDowntimeJailDuration))
timeout := uint64(types.GetTimeoutTimestamp(oldBlockTime).UnixNano())
packet := channeltypes.NewPacket(packetData.GetBytes(), 1, childtypes.PortID, s.path.EndpointA.ChannelID,
parenttypes.PortID, s.path.EndpointB.ChannelID, clienttypes.Height{}, timeout)

// Send the downtime packet through CCV
err = s.path.EndpointA.SendPacket(packet)
s.Require().NoError(err)

// commit block on the child chain and update the parent's client state
s.childChain.App.EndBlock(abci.RequestEndBlock{})
s.coordinator.CommitBlock(s.childChain)
err = s.path.EndpointB.UpdateClient()
s.Require().NoError(err)

// receive the downtime packet on the provider chain
s.path.EndpointB.RecvPacket(packet)

// validator should be jailed on the provider chain
valAddr, err := sdk.ValAddressFromHex(tmVal.Address.String())
s.Require().NoError(err)
validatorJailed, ok := s.parentChain.App.GetStakingKeeper().GetValidator(s.parentCtx(), valAddr)
s.Require().True(ok)
s.Require().True(validatorJailed.Jailed)
s.Require().Equal(validatorJailed.Status, stakingtypes.Unbonding)

// validator should have been slashed
slashedAmout := sdk.NewDec(1).QuoInt64(slashFraction).Mul(valOldBalance.ToDec())
resultingTokens := valOldBalance.Sub(slashedAmout.TruncateInt())
s.Require().Equal(resultingTokens, validatorJailed.GetTokens())

// validator JailedUntil timestamp should be in the future
valSignInfo, found := parentSlashingKeeper.GetValidatorSigningInfo(s.parentCtx(), consAddr)
s.Require().True(found)
s.Require().True(valSignInfo.JailedUntil.After(s.parentCtx().BlockHeader().Time))
}

// TestHandleConsumerDowntime tests the slashing and jailing on the provider
// by varying the downtime infraction block height
func (s *ParentTestSuite) TestHandleConsumerDowntime() {
s.SetupCCVChannel()

parentStakingKeeper := s.parentChain.App.GetStakingKeeper()
parentSlashingKeeper := s.parentChain.App.(*app.App).SlashingKeeper

delAddr := s.parentChain.SenderAccount.GetAddress()

// Should return an error when the validator doesn't exists
err := s.parentChain.App.(*app.App).ParentKeeper.HandleConsumerDowntime(
s.parentCtx(),
types.NewValidatorDowtimePacketData(
abci.Validator{Address: bytes.HexBytes{}, Power: int64(1)}, uint64(1), int64(4), int64(1),
),
)
s.Require().Error(err)

// Choose a validator, and get its address and data structure into the correct types
tmValidator := s.parentChain.Vals.Validators[0]
valAddr := sdk.ValAddress(tmValidator.Address)

// Create a signing info to jail the validator for downtime
valInfo := slashingtypes.NewValidatorSigningInfo(sdk.ConsAddress(tmValidator.Address), s.parentCtx().BlockHeight(),
s.parentCtx().BlockHeight()-1, time.Time{}.UTC(), false, int64(0))
parentSlashingKeeper.SetValidatorSigningInfo(s.parentCtx(), sdk.ConsAddress(tmValidator.Address), valInfo)

// Undelegate the shares from the validator
ubdTime := time.Now()
parentCtx := s.parentCtx().WithBlockTime(ubdTime)
parentStakingKeeper.Undelegate(parentCtx, delAddr, valAddr, sdk.NewDec(1))

// The undelegation creates a change in the valset thus
// the valset update ID is saved with the current block height
s.parentChain.App.EndBlock(abci.RequestEndBlock{})

// Save unbonding balance before slashing
ubd, found := parentStakingKeeper.GetUnbondingDelegation(parentCtx, delAddr, valAddr)
s.Require().Len(ubd.Entries, 1)
s.Require().True(found)
ubdBalance := ubd.Entries[0].Balance

// test the slashing at different block time and height
testCases := []struct {
blockHeight int64
currentTime time.Time
expSlashAmount sdk.Int
}{{
blockHeight: s.parentCtx().BlockHeight(),
currentTime: ubdTime.Add(childtypes.UnbondingTime).Add(3 * time.Hour),
expSlashAmount: sdk.NewInt(0),
},
{
blockHeight: s.parentCtx().BlockHeight() + 1,
currentTime: ubdTime.Add(childtypes.UnbondingTime).Add(3 * time.Hour),
expSlashAmount: sdk.NewInt(0),
},
{
blockHeight: s.parentCtx().BlockHeight() + 1,
currentTime: ubdTime.Add(3 * time.Hour),
expSlashAmount: ubdBalance.ToDec().Mul(sdk.NewDec(1).QuoInt64(4)).TruncateInt(),
},
}

for _, tc := range testCases {
parentCtx = s.parentCtx().WithBlockHeight(tc.blockHeight).WithBlockTime(tc.currentTime)
// Slash 1/4 of the validator tokens
err := s.parentChain.App.(*app.App).ParentKeeper.HandleConsumerDowntime(
parentCtx,
types.NewValidatorDowtimePacketData(
abci.Validator{Address: tmValidator.Address, Power: int64(1)}, uint64(1), int64(4), int64(1),
),
)
s.Require().NoError(err)

newUbdBalance, found := parentStakingKeeper.GetUnbondingDelegation(parentCtx, delAddr, valAddr)
s.Require().Len(newUbdBalance.Entries, 1)
s.Require().True(found)

s.Require().True(tc.expSlashAmount.Abs().Equal(ubdBalance.Sub(newUbdBalance.Entries[0].Balance)))
}
}
9 changes: 9 additions & 0 deletions x/ccv/parent/types/keys.go
Original file line number Diff line number Diff line change
@@ -49,6 +49,9 @@ const (
// ChildChainToUBDEsPrefix is for the index for looking up which unbonding delegation entries are waiting for a given
// child chain to unbond
UBDEIndexPrefix = "childchaintoubdes"

//ValsetUpdateBlockHeightPrefix
ValsetUpdateBlockHeightPrefix = "valsetupdateblockheigt"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ValsetUpdateBlockHeightPrefix = "valsetupdateblockheigt"
ValsetUpdateBlockHeightPrefix = "valsetupdateblockheight"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sainoe patch this on main

)

var (
@@ -110,3 +113,9 @@ func UnbondingDelegationEntryKey(unbondingDelegationEntryID uint64) []byte {

return append([]byte(UnbondingDelegationEntryPrefix), bz...)
}

func ValsetUpdateBlockHeightKey(valsetUpdateId uint64) []byte {
vuidBytes := make([]byte, 8)
binary.BigEndian.PutUint64(vuidBytes, valsetUpdateId)
return append([]byte(ValsetUpdateBlockHeightPrefix), vuidBytes...)
}
34 changes: 34 additions & 0 deletions x/ccv/types/ccv.go
Original file line number Diff line number Diff line change
@@ -24,3 +24,37 @@ func (vsc ValidatorSetChangePacketData) GetBytes() []byte {
valUpdateBytes := ModuleCdc.MustMarshalJSON(&vsc)
return valUpdateBytes
}

func NewValidatorDowtimePacketData(validator abci.Validator, valUpdateId uint64, slashFraction, jailTime int64) ValidatorDowntimePacketData {
return ValidatorDowntimePacketData{
Validator: validator,
SlashFraction: slashFraction,
JailTime: jailTime,
ValsetUpdateId: valUpdateId,
}
}

func (vdt ValidatorDowntimePacketData) ValidateBasic() error {
if len(vdt.Validator.Address) == 0 || vdt.Validator.Power == 0 {
return sdkerrors.Wrap(ErrInvalidPacketData, "validator fields cannot be empty")
}

if vdt.JailTime <= 0 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should allow slash with 0 jail time right?

return sdkerrors.Wrap(ErrInvalidPacketData, "jail duration must be positive")
}

if vdt.SlashFraction <= 0 {
return sdkerrors.Wrap(ErrInvalidPacketData, "slash fraction must be positive")
}

if vdt.ValsetUpdateId == 0 {
return sdkerrors.Wrap(ErrInvalidPacketData, "valset update id cannot be equal to zero")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sainoe think about the 0/nil issue more and let me know if valsetupdate IDs need to start at 1 globally

}

return nil
}

func (vdt ValidatorDowntimePacketData) GetBytes() []byte {
valDowntimeBytes := ModuleCdc.MustMarshalJSON(&vdt)
return valDowntimeBytes
}
351 changes: 318 additions & 33 deletions x/ccv/types/ccv.pb.go
20 changes: 20 additions & 0 deletions x/ccv/types/expected_keepers.go
Original file line number Diff line number Diff line change
@@ -6,6 +6,8 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"

capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"
slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types"
"github.com/cosmos/cosmos-sdk/x/staking/types"
conntypes "github.com/cosmos/ibc-go/modules/core/03-connection/types"
channeltypes "github.com/cosmos/ibc-go/modules/core/04-channel/types"
ibcexported "github.com/cosmos/ibc-go/modules/core/exported"
@@ -20,6 +22,19 @@ type StakingKeeper interface {
GetValidatorUpdates(ctx sdk.Context) []abci.ValidatorUpdate
CompleteStoppedUnbonding(ctx sdk.Context, id uint64) (found bool, err error)
UnbondingTime(ctx sdk.Context) time.Duration
GetValidatorByConsAddr(ctx sdk.Context, consAddr sdk.ConsAddress) (validator types.Validator, found bool)
// slash the validator and delegators of the validator, specifying offence height, offence power, and slash fraction
Jail(sdk.Context, sdk.ConsAddress) // jail a validator
Slash(sdk.Context, sdk.ConsAddress, int64, int64, sdk.Dec)
}

type SlashingKeeper interface {
JailUntil(sdk.Context, sdk.ConsAddress, time.Time)
GetValidatorSigningInfo(ctx sdk.Context, address sdk.ConsAddress) (info slashingtypes.ValidatorSigningInfo, found bool)
SetValidatorSigningInfo(ctx sdk.Context, address sdk.ConsAddress, info slashingtypes.ValidatorSigningInfo)
DowntimeJailDuration(sdk.Context) time.Duration
SlashFractionDowntime(sdk.Context) sdk.Dec
ClearValidatorMissedBlockBitArray(ctx sdk.Context, address sdk.ConsAddress)
}

// ChannelKeeper defines the expected IBC channel keeper
@@ -49,3 +64,8 @@ type ClientKeeper interface {
}

// TODO: Expected interfaces for distribution on parent and baby chains
// SlashingHooks event hooks for jailing and slashing validator
type SlashingHooks interface {
// Is triggered when the validator missed too many blocks
AfterValidatorDowntime(ctx sdk.Context, consAddr sdk.ConsAddress, power int64)
}
File renamed without changes.