diff --git a/app/upgrades.go b/app/upgrades.go index d4a385a36a..2724d5f1f5 100644 --- a/app/upgrades.go +++ b/app/upgrades.go @@ -89,6 +89,19 @@ func (app *App) RegisterUpgradeHandlers() { return newVM, err } + // IBC Toggle migration: initialize InboundEnabled and OutboundEnabled params + if upgradeName == "v6.4.0" { + newVM, err := app.mm.RunMigrations(ctx, app.configurator, fromVM) + if err != nil { + return newVM, err + } + + // Enable IBC inbound and outbound by default + app.IBCKeeper.SetInboundEnabled(ctx, true) + app.IBCKeeper.SetOutboundEnabled(ctx, true) + return newVM, err + } + return app.mm.RunMigrations(ctx, app.configurator, fromVM) }) } diff --git a/sei-ibc-go/modules/core/02-client/keeper/client.go b/sei-ibc-go/modules/core/02-client/keeper/client.go index 22d39795b9..ce984872a9 100644 --- a/sei-ibc-go/modules/core/02-client/keeper/client.go +++ b/sei-ibc-go/modules/core/02-client/keeper/client.go @@ -12,11 +12,19 @@ import ( "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/core/exported" ) +// ErrInboundDisabled is the error for when inbound is disabled +var ErrInboundDisabled = sdkerrors.Register("ibc-client", 101, "ibc inbound disabled") + // CreateClient creates a new client state and populates it with a given consensus // state as defined in https://github.com/cosmos/ibc/tree/master/spec/core/ics-002-client-semantics#create func (k Keeper) CreateClient( ctx sdk.Context, clientState exported.ClientState, consensusState exported.ConsensusState, ) (string, error) { + // inbound gating: disallow client creation as part of inbound handshakes when inbound disabled + if !k.IsInboundEnabled(ctx) { + return "", sdkerrors.Wrap(ErrInboundDisabled, "client creation inbound disabled") + } + params := k.GetParams(ctx) if !params.IsAllowedClient(clientState.ClientType()) { return "", sdkerrors.Wrapf( diff --git a/sei-ibc-go/modules/core/02-client/keeper/client_test.go b/sei-ibc-go/modules/core/02-client/keeper/client_test.go index f888b888ea..e4855b0055 100644 --- a/sei-ibc-go/modules/core/02-client/keeper/client_test.go +++ b/sei-ibc-go/modules/core/02-client/keeper/client_test.go @@ -41,6 +41,27 @@ func (suite *KeeperTestSuite) TestCreateClient() { } } +// TestCreateClient_BlockedWhenInboundDisabled tests that CreateClient +// is blocked when inbound IBC is disabled. +func (suite *KeeperTestSuite) TestCreateClient_BlockedWhenInboundDisabled() { + // disable inbound on chainA + ibcKeeperA := suite.chainA.App.GetIBCKeeper() + ibcKeeperA.SetInboundEnabled(suite.chainA.GetContext(), false) + suite.Require().False(ibcKeeperA.IsInboundEnabled(suite.chainA.GetContext())) + + clientState := ibctmtypes.NewClientState(testChainID, ibctmtypes.DefaultTrustLevel, trustingPeriod, ubdPeriod, maxClockDrift, testClientHeight, commitmenttypes.GetSDKSpecs(), ibctesting.UpgradePath, false, false) + + // attempt CreateClient with inbound disabled + clientID, err := suite.chainA.App.GetIBCKeeper().ClientKeeper.CreateClient( + suite.chainA.GetContext(), clientState, suite.consensusState, + ) + + // should fail with ErrInboundDisabled + suite.Require().Error(err) + suite.Require().Contains(err.Error(), "inbound") + suite.Require().Equal("", clientID) +} + func (suite *KeeperTestSuite) TestUpdateClientTendermint() { var ( path *ibctesting.Path diff --git a/sei-ibc-go/modules/core/02-client/keeper/keeper.go b/sei-ibc-go/modules/core/02-client/keeper/keeper.go index c9160e6f0c..69f579d115 100644 --- a/sei-ibc-go/modules/core/02-client/keeper/keeper.go +++ b/sei-ibc-go/modules/core/02-client/keeper/keeper.go @@ -22,6 +22,9 @@ import ( ibctmtypes "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/light-clients/07-tendermint/types" ) +// KeyInboundEnabled is the param key for inbound enabled +var KeyInboundEnabled = []byte("InboundEnabled") + // Keeper represents a type that grants read and write permissions to any client // state information type Keeper struct { @@ -53,6 +56,13 @@ func (k Keeper) Logger(ctx sdk.Context) log.Logger { return ctx.Logger().With("module", "x/"+host.ModuleName+"/"+types.SubModuleName) } +// IsInboundEnabled returns true if inbound IBC is enabled. +func (k Keeper) IsInboundEnabled(ctx sdk.Context) bool { + var inbound bool + k.paramSpace.Get(ctx, KeyInboundEnabled, &inbound) + return inbound +} + // GenerateClientIdentifier returns the next client identifier. func (k Keeper) GenerateClientIdentifier(ctx sdk.Context, clientType string) string { nextClientSeq := k.GetNextClientSequence(ctx) diff --git a/sei-ibc-go/modules/core/03-connection/keeper/handshake.go b/sei-ibc-go/modules/core/03-connection/keeper/handshake.go index 7be9e27253..4c0c96e625 100644 --- a/sei-ibc-go/modules/core/03-connection/keeper/handshake.go +++ b/sei-ibc-go/modules/core/03-connection/keeper/handshake.go @@ -26,6 +26,11 @@ func (k Keeper) ConnOpenInit( version *types.Version, delayPeriod uint64, ) (string, error) { + // outbound gating: disallow outbound connection inits when outbound disabled + if !k.IsOutboundEnabled(ctx) { + return "", sdkerrors.Wrap(ErrOutboundDisabled, "connection outbound disabled") + } + versions := types.GetCompatibleVersions() if version != nil { if !types.IsSupportedVersion(version) { @@ -75,6 +80,11 @@ func (k Keeper) ConnOpenTry( proofHeight exported.Height, // height at which relayer constructs proof of A storing connectionEnd in state consensusHeight exported.Height, // latest height of chain B which chain A has stored in its chain B client ) (string, error) { + // inbound gating: disallow inbound connection tries when inbound disabled + if !k.IsInboundEnabled(ctx) { + return "", sdkerrors.Wrap(ErrInboundDisabled, "connection inbound disabled") + } + var ( connectionID string previousConnection types.ConnectionEnd diff --git a/sei-ibc-go/modules/core/03-connection/keeper/handshake_test.go b/sei-ibc-go/modules/core/03-connection/keeper/handshake_test.go index 3bb0d4eba2..06560c2a45 100644 --- a/sei-ibc-go/modules/core/03-connection/keeper/handshake_test.go +++ b/sei-ibc-go/modules/core/03-connection/keeper/handshake_test.go @@ -600,6 +600,79 @@ func (suite *KeeperTestSuite) TestConnOpenAck() { } } +// TestConnOpenTry_BlockedWhenInboundDisabled tests that ConnOpenTry +// is blocked when inbound IBC is disabled. +func (suite *KeeperTestSuite) TestConnOpenTry_BlockedWhenInboundDisabled() { + suite.SetupTest() + + path := ibctesting.NewPath(suite.chainA, suite.chainB) + suite.coordinator.SetupClients(path) + + // chainA initiates connection + err := path.EndpointA.ConnOpenInit() + suite.Require().NoError(err) + + // retrieve client state of chainA to pass as counterpartyClient + counterpartyClient := suite.chainA.GetClientState(path.EndpointA.ClientID) + + // disable inbound on chainB + ibcKeeperB := suite.chainB.App.GetIBCKeeper() + ibcKeeperB.SetInboundEnabled(suite.chainB.GetContext(), false) + suite.Require().False(ibcKeeperB.IsInboundEnabled(suite.chainB.GetContext())) + + // ensure client is up to date on chainB + err = path.EndpointB.UpdateClient() + suite.Require().NoError(err) + + counterparty := types.NewCounterparty(path.EndpointA.ClientID, path.EndpointA.ConnectionID, suite.chainA.GetPrefix()) + + connectionKey := host.ConnectionKey(path.EndpointA.ConnectionID) + proofInit, proofHeight := suite.chainA.QueryProof(connectionKey) + + consensusHeight := counterpartyClient.GetLatestHeight() + consensusKey := host.FullConsensusStateKey(path.EndpointA.ClientID, consensusHeight) + proofConsensus, _ := suite.chainA.QueryProof(consensusKey) + + clientKey := host.FullClientStateKey(path.EndpointA.ClientID) + proofClient, _ := suite.chainA.QueryProof(clientKey) + + // attempt ConnOpenTry on chainB with inbound disabled + _, err = suite.chainB.App.GetIBCKeeper().ConnectionKeeper.ConnOpenTry( + suite.chainB.GetContext(), "", counterparty, 0, path.EndpointB.ClientID, counterpartyClient, + types.GetCompatibleVersions(), proofInit, proofClient, proofConsensus, + proofHeight, consensusHeight, + ) + + // should fail with ErrInboundDisabled + suite.Require().Error(err) + suite.Require().Contains(err.Error(), "inbound") +} + +// TestConnOpenInit_BlockedWhenOutboundDisabled tests that ConnOpenInit +// is blocked when outbound IBC is disabled. +func (suite *KeeperTestSuite) TestConnOpenInit_BlockedWhenOutboundDisabled() { + suite.SetupTest() + + path := ibctesting.NewPath(suite.chainA, suite.chainB) + suite.coordinator.SetupClients(path) + + // disable outbound on chainA + ibcKeeperA := suite.chainA.App.GetIBCKeeper() + ibcKeeperA.SetOutboundEnabled(suite.chainA.GetContext(), false) + suite.Require().False(ibcKeeperA.IsOutboundEnabled(suite.chainA.GetContext())) + + counterparty := types.NewCounterparty(path.EndpointB.ClientID, "", suite.chainB.GetPrefix()) + + // attempt ConnOpenInit on chainA with outbound disabled + _, err := suite.chainA.App.GetIBCKeeper().ConnectionKeeper.ConnOpenInit( + suite.chainA.GetContext(), path.EndpointA.ClientID, counterparty, types.DefaultIBCVersion, 0, + ) + + // should fail with ErrOutboundDisabled + suite.Require().Error(err) + suite.Require().Contains(err.Error(), "outbound") +} + // TestConnOpenConfirm - chainB calls ConnOpenConfirm to confirm that // chainA state is now OPEN. func (suite *KeeperTestSuite) TestConnOpenConfirm() { diff --git a/sei-ibc-go/modules/core/03-connection/keeper/keeper.go b/sei-ibc-go/modules/core/03-connection/keeper/keeper.go index 1c1b55b380..890f08b97d 100644 --- a/sei-ibc-go/modules/core/03-connection/keeper/keeper.go +++ b/sei-ibc-go/modules/core/03-connection/keeper/keeper.go @@ -15,6 +15,18 @@ import ( "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/core/exported" ) +// KeyInboundEnabled is the param key for inbound enabled +var KeyInboundEnabled = []byte("InboundEnabled") + +// KeyOutboundEnabled is the param key for outbound enabled +var KeyOutboundEnabled = []byte("OutboundEnabled") + +// ErrInboundDisabled is the error for when inbound is disabled +var ErrInboundDisabled = sdkerrors.Register("ibc-connection", 101, "ibc inbound disabled") + +// ErrOutboundDisabled is the error for when outbound is disabled +var ErrOutboundDisabled = sdkerrors.Register("ibc-connection", 102, "ibc outbound disabled") + // Keeper defines the IBC connection keeper type Keeper struct { // implements gRPC QueryServer interface @@ -46,6 +58,20 @@ func (k Keeper) Logger(ctx sdk.Context) log.Logger { return ctx.Logger().With("module", "x/"+host.ModuleName+"/"+types.SubModuleName) } +// IsInboundEnabled returns true if inbound IBC is enabled. +func (k Keeper) IsInboundEnabled(ctx sdk.Context) bool { + var inbound bool + k.paramSpace.Get(ctx, KeyInboundEnabled, &inbound) + return inbound +} + +// IsOutboundEnabled returns true if outbound IBC is enabled. +func (k Keeper) IsOutboundEnabled(ctx sdk.Context) bool { + var outbound bool + k.paramSpace.Get(ctx, KeyOutboundEnabled, &outbound) + return outbound +} + // GetCommitmentPrefix returns the IBC connection store prefix as a commitment // Prefix func (k Keeper) GetCommitmentPrefix() exported.Prefix { diff --git a/sei-ibc-go/modules/core/04-channel/keeper/handshake.go b/sei-ibc-go/modules/core/04-channel/keeper/handshake.go index f74296bced..f6c9794b1e 100644 --- a/sei-ibc-go/modules/core/04-channel/keeper/handshake.go +++ b/sei-ibc-go/modules/core/04-channel/keeper/handshake.go @@ -15,6 +15,12 @@ import ( "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/core/exported" ) +// ErrInboundDisabledHandshake is the error for when inbound is disabled during channel handshake +var ErrInboundDisabledHandshake = sdkerrors.Register("ibc-channel-handshake", 101, "ibc inbound disabled") + +// ErrOutboundDisabledHandshake is the error for when outbound is disabled during channel handshake +var ErrOutboundDisabledHandshake = sdkerrors.Register("ibc-channel-handshake", 102, "ibc outbound disabled") + // ChanOpenInit is called by a module to initiate a channel opening handshake with // a module on another chain. The counterparty channel identifier is validated to be // empty in msg validation. @@ -27,6 +33,11 @@ func (k Keeper) ChanOpenInit( counterparty types.Counterparty, version string, ) (string, *capabilitytypes.Capability, error) { + // outbound gating: disallow outbound channel inits when outbound disabled + if !k.IsOutboundEnabled(ctx) { + return "", nil, sdkerrors.Wrap(ErrOutboundDisabledHandshake, "channel outbound disabled") + } + // connection hop length checked on msg.ValidateBasic() connectionEnd, found := k.connectionKeeper.GetConnection(ctx, connectionHops[0]) if !found { @@ -106,6 +117,11 @@ func (k Keeper) ChanOpenTry( proofInit []byte, proofHeight exported.Height, ) (string, *capabilitytypes.Capability, error) { + // inbound gating: disallow inbound channel tries when inbound disabled + if !k.IsInboundEnabled(ctx) { + return "", nil, sdkerrors.Wrap(ErrInboundDisabledHandshake, "channel inbound disabled") + } + var ( previousChannel types.Channel previousChannelFound bool diff --git a/sei-ibc-go/modules/core/04-channel/keeper/handshake_test.go b/sei-ibc-go/modules/core/04-channel/keeper/handshake_test.go index f5182eba73..17d73092ce 100644 --- a/sei-ibc-go/modules/core/04-channel/keeper/handshake_test.go +++ b/sei-ibc-go/modules/core/04-channel/keeper/handshake_test.go @@ -307,6 +307,80 @@ func (suite *KeeperTestSuite) TestChanOpenTry() { } } +// TestChanOpenTry_BlockedWhenInboundDisabled tests that ChanOpenTry +// is blocked when inbound IBC is disabled. +func (suite *KeeperTestSuite) TestChanOpenTry_BlockedWhenInboundDisabled() { + suite.SetupTest() + + path := ibctesting.NewPath(suite.chainA, suite.chainB) + suite.coordinator.SetupConnections(path) + path.SetChannelOrdered() + + // chainA opens channel + err := path.EndpointA.ChanOpenInit() + suite.Require().NoError(err) + + // create port capability on chainB + suite.chainB.CreatePortCapability(suite.chainB.GetSimApp().ScopedIBCMockKeeper, ibctesting.MockPort) + portCap := suite.chainB.GetPortCapability(ibctesting.MockPort) + + // disable inbound on chainB + ibcKeeperB := suite.chainB.App.GetIBCKeeper() + ibcKeeperB.SetInboundEnabled(suite.chainB.GetContext(), false) + suite.Require().False(ibcKeeperB.IsInboundEnabled(suite.chainB.GetContext())) + + // ensure client is up to date on chainB + err = path.EndpointB.UpdateClient() + suite.Require().NoError(err) + + counterparty := types.NewCounterparty(path.EndpointB.ChannelConfig.PortID, ibctesting.FirstChannelID) + + channelKey := host.ChannelKey(counterparty.PortId, counterparty.ChannelId) + proof, proofHeight := suite.chainA.QueryProof(channelKey) + + // attempt ChanOpenTry on chainB with inbound disabled + _, _, err = suite.chainB.App.GetIBCKeeper().ChannelKeeper.ChanOpenTry( + suite.chainB.GetContext(), types.ORDERED, []string{path.EndpointB.ConnectionID}, + path.EndpointB.ChannelConfig.PortID, "", portCap, counterparty, path.EndpointA.ChannelConfig.Version, + proof, proofHeight, + ) + + // should fail with ErrInboundDisabled + suite.Require().Error(err) + suite.Require().Contains(err.Error(), "inbound") +} + +// TestChanOpenInit_BlockedWhenOutboundDisabled tests that ChanOpenInit +// is blocked when outbound IBC is disabled. +func (suite *KeeperTestSuite) TestChanOpenInit_BlockedWhenOutboundDisabled() { + suite.SetupTest() + + path := ibctesting.NewPath(suite.chainA, suite.chainB) + suite.coordinator.SetupConnections(path) + path.SetChannelOrdered() + + // create port capability on chainA + suite.chainA.CreatePortCapability(suite.chainA.GetSimApp().ScopedIBCMockKeeper, ibctesting.MockPort) + portCap := suite.chainA.GetPortCapability(ibctesting.MockPort) + + // disable outbound on chainA + ibcKeeperA := suite.chainA.App.GetIBCKeeper() + ibcKeeperA.SetOutboundEnabled(suite.chainA.GetContext(), false) + suite.Require().False(ibcKeeperA.IsOutboundEnabled(suite.chainA.GetContext())) + + counterparty := types.NewCounterparty(ibctesting.MockPort, "") + + // attempt ChanOpenInit on chainA with outbound disabled + _, _, err := suite.chainA.App.GetIBCKeeper().ChannelKeeper.ChanOpenInit( + suite.chainA.GetContext(), types.ORDERED, []string{path.EndpointA.ConnectionID}, + path.EndpointA.ChannelConfig.PortID, portCap, counterparty, path.EndpointA.ChannelConfig.Version, + ) + + // should fail with ErrOutboundDisabled + suite.Require().Error(err) + suite.Require().Contains(err.Error(), "outbound") +} + // TestChanOpenAck tests the OpenAck handshake call for channels. It uses message passing // to enter into the appropriate state and then calls ChanOpenAck directly. The handshake // call is occurring on chainA. diff --git a/sei-ibc-go/modules/core/04-channel/keeper/keeper.go b/sei-ibc-go/modules/core/04-channel/keeper/keeper.go index 476c1733dc..4dc0069bb4 100644 --- a/sei-ibc-go/modules/core/04-channel/keeper/keeper.go +++ b/sei-ibc-go/modules/core/04-channel/keeper/keeper.go @@ -9,6 +9,7 @@ import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" capabilitykeeper "github.com/cosmos/cosmos-sdk/x/capability/keeper" capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" "github.com/tendermint/tendermint/libs/log" db "github.com/tendermint/tm-db" @@ -29,6 +30,7 @@ type Keeper struct { storeKey sdk.StoreKey cdc codec.BinaryCodec + paramSpace paramtypes.Subspace clientKeeper types.ClientKeeper connectionKeeper types.ConnectionKeeper portKeeper types.PortKeeper @@ -37,13 +39,14 @@ type Keeper struct { // NewKeeper creates a new IBC channel Keeper instance func NewKeeper( - cdc codec.BinaryCodec, key sdk.StoreKey, + cdc codec.BinaryCodec, key sdk.StoreKey, paramSpace paramtypes.Subspace, clientKeeper types.ClientKeeper, connectionKeeper types.ConnectionKeeper, portKeeper types.PortKeeper, scopedKeeper capabilitykeeper.ScopedKeeper, ) Keeper { return Keeper{ storeKey: key, cdc: cdc, + paramSpace: paramSpace, clientKeeper: clientKeeper, connectionKeeper: connectionKeeper, portKeeper: portKeeper, @@ -51,6 +54,26 @@ func NewKeeper( } } +// KeyOutboundEnabled is the param key for outbound enabled +var KeyOutboundEnabled = []byte("OutboundEnabled") + +// KeyInboundEnabled is the param key for inbound enabled +var KeyInboundEnabled = []byte("InboundEnabled") + +// IsOutboundEnabled returns true if outbound IBC is enabled. +func (k Keeper) IsOutboundEnabled(ctx sdk.Context) bool { + var outbound bool + k.paramSpace.Get(ctx, KeyOutboundEnabled, &outbound) + return outbound +} + +// IsInboundEnabled returns true if inbound IBC is enabled. +func (k Keeper) IsInboundEnabled(ctx sdk.Context) bool { + var inbound bool + k.paramSpace.Get(ctx, KeyInboundEnabled, &inbound) + return inbound +} + // Logger returns a module-specific logger. func (k Keeper) Logger(ctx sdk.Context) log.Logger { return ctx.Logger().With("module", "x/"+host.ModuleName+"/"+types.SubModuleName) diff --git a/sei-ibc-go/modules/core/04-channel/keeper/packet.go b/sei-ibc-go/modules/core/04-channel/keeper/packet.go index bd6db3fcc0..55d1e5c832 100644 --- a/sei-ibc-go/modules/core/04-channel/keeper/packet.go +++ b/sei-ibc-go/modules/core/04-channel/keeper/packet.go @@ -15,6 +15,12 @@ import ( "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/core/exported" ) +// ErrOutboundDisabled is the error for when outbound is disabled +var ErrOutboundDisabled = sdkerrors.Register("ibc-channel", 102, "ibc outbound disabled") + +// ErrInboundDisabled is the error for when inbound is disabled +var ErrInboundDisabled = sdkerrors.Register("ibc-channel", 103, "ibc inbound disabled") + // SendPacket is called by a module in order to send an IBC packet on a channel // end owned by the calling module to the corresponding module on the counterparty // chain. @@ -23,6 +29,11 @@ func (k Keeper) SendPacket( channelCap *capabilitytypes.Capability, packet exported.PacketI, ) error { + // outbound gating: disallow sending packets when outbound disabled + if !k.IsOutboundEnabled(ctx) { + return sdkerrors.Wrap(ErrOutboundDisabled, "ibc outbound disabled") + } + if err := packet.ValidateBasic(); err != nil { return sdkerrors.Wrap(err, "packet failed basic validation") } @@ -157,6 +168,11 @@ func (k Keeper) RecvPacket( proof []byte, proofHeight exported.Height, ) error { + // inbound gating: disallow processing inbound packets when inbound disabled + if !k.IsInboundEnabled(ctx) { + return sdkerrors.Wrap(ErrInboundDisabled, "ibc inbound disabled") + } + channel, found := k.GetChannel(ctx, packet.GetDestPort(), packet.GetDestChannel()) if !found { return sdkerrors.Wrap(types.ErrChannelNotFound, packet.GetDestChannel()) diff --git a/sei-ibc-go/modules/core/04-channel/keeper/packet_test.go b/sei-ibc-go/modules/core/04-channel/keeper/packet_test.go index 0cd10a1e51..decc988c83 100644 --- a/sei-ibc-go/modules/core/04-channel/keeper/packet_test.go +++ b/sei-ibc-go/modules/core/04-channel/keeper/packet_test.go @@ -491,6 +491,46 @@ func (suite *KeeperTestSuite) TestRecvPacket() { } } +// TestRecvPacket_BlockedWhenInboundDisabled tests that RecvPacket +// is blocked when inbound IBC is disabled. +func (suite *KeeperTestSuite) TestRecvPacket_BlockedWhenInboundDisabled() { + suite.SetupTest() + + path := ibctesting.NewPath(suite.chainA, suite.chainB) + path.SetChannelOrdered() + suite.coordinator.Setup(path) + + // prepare packet from A -> B + packet := types.NewPacket(ibctesting.MockPacketData, 1, + path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, + path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, + timeoutHeight, disabledTimeoutTimestamp, + ) + + err := path.EndpointA.SendPacket(packet) + suite.Require().NoError(err) + + // disable inbound on chainB + ibcKeeperB := suite.chainB.App.GetIBCKeeper() + ibcKeeperB.SetInboundEnabled(suite.chainB.GetContext(), false) + suite.Require().False(ibcKeeperB.IsInboundEnabled(suite.chainB.GetContext())) + + // get proof of packet commitment from chainA + packetKey := host.PacketCommitmentKey(packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence()) + proof, proofHeight := path.EndpointA.QueryProof(packetKey) + + channelCap := suite.chainB.GetChannelCapability(path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID) + + // attempt RecvPacket with inbound disabled + err = suite.chainB.App.GetIBCKeeper().ChannelKeeper.RecvPacket( + suite.chainB.GetContext(), channelCap, packet, proof, proofHeight, + ) + + // should fail with ErrInboundDisabled + suite.Require().Error(err) + suite.Require().Contains(err.Error(), "inbound") +} + func (suite *KeeperTestSuite) TestWriteAcknowledgement() { var ( path *ibctesting.Path diff --git a/sei-ibc-go/modules/core/genesis.go b/sei-ibc-go/modules/core/genesis.go index 82c4c0c0ea..c0d1fa1e43 100644 --- a/sei-ibc-go/modules/core/genesis.go +++ b/sei-ibc-go/modules/core/genesis.go @@ -13,6 +13,9 @@ import ( // InitGenesis initializes the ibc state from a provided genesis // state. func InitGenesis(ctx sdk.Context, k keeper.Keeper, createLocalhost bool, gs *types.GenesisState) { + // Initialize core params with defaults if not set + k.SetParams(ctx, types.DefaultParams()) + client.InitGenesis(ctx, k.ClientKeeper, gs.ClientGenesis) connection.InitGenesis(ctx, k.ConnectionKeeper, gs.ConnectionGenesis) channel.InitGenesis(ctx, k.ChannelKeeper, gs.ChannelGenesis) diff --git a/sei-ibc-go/modules/core/keeper/inbound_test.go b/sei-ibc-go/modules/core/keeper/inbound_test.go new file mode 100644 index 0000000000..367b2a7e8c --- /dev/null +++ b/sei-ibc-go/modules/core/keeper/inbound_test.go @@ -0,0 +1,180 @@ +package keeper_test + +import ( + "strings" + + sdk "github.com/cosmos/cosmos-sdk/types" + + clienttypes "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/core/02-client/types" + connectiontypes "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/core/03-connection/types" + channeltypes "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/core/04-channel/types" + host "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/core/24-host" + "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/core/keeper" + ibctesting "github.com/sei-protocol/sei-chain/sei-ibc-go/testing" +) + +// Test that the default configuration for inbound is enabled. +func (suite *KeeperTestSuite) TestInbound_DefaultEnabled() { + ctx := suite.chainA.GetContext() + ik := suite.chainA.App.GetIBCKeeper() + + enabled := ik.IsInboundEnabled(ctx) + suite.Require().True(enabled, "expected inbound IBC to be enabled by default") +} + +// Test that RecvPacket is blocked when inbound is disabled. +func (suite *KeeperTestSuite) TestRecvPacket_BlockedWhenInboundDisabled() { + suite.SetupTest() // reset + + path := ibctesting.NewPath(suite.chainA, suite.chainB) + path.SetChannelOrdered() + suite.coordinator.Setup(path) + + // prepare packet from A -> B + timeoutHeight := suite.chainB.GetTimeoutHeight() + packet := channeltypes.NewPacket(ibctesting.MockPacketData, 1, + path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, + path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, + timeoutHeight, 0, + ) + + err := path.EndpointA.SendPacket(packet) + suite.Require().NoError(err) + + // disable inbound on chainB using top-level keeper + ibcKeeperB := suite.chainB.App.GetIBCKeeper() + ibcKeeperB.SetInboundEnabled(suite.chainB.GetContext(), false) + suite.Require().False(ibcKeeperB.IsInboundEnabled(suite.chainB.GetContext()), "expected inbound to be disabled") + + // fetch proof of packet commitment from chainA + packetKey := host.PacketCommitmentKey(packet.GetSourcePort(), packet.GetSourceChannel(), packet.GetSequence()) + proof, proofHeight := path.EndpointA.QueryProof(packetKey) + + // craft MsgRecvPacket as other tests do + msg := channeltypes.NewMsgRecvPacket(packet, proof, proofHeight, suite.chainB.SenderAccount.GetAddress().String()) + + // call the gRPC/keeper RecvPacket and expect an error because inbound is disabled + _, err = keeper.Keeper.RecvPacket(*ibcKeeperB, sdk.WrapSDKContext(suite.chainB.GetContext()), msg) + suite.Require().Error(err, "expected RecvPacket to return an error when inbound is disabled") + suite.Require().True(strings.Contains(strings.ToLower(err.Error()), "inbound"), + "expected error to mention inbound/disabled, got: %s", err.Error()) +} + +// TestAcknowledgementAllowedWhenOutboundDisabled verifies that MsgAcknowledgement +// succeeds even when OutboundEnabled == false (settlement must be allowed). +func (suite *KeeperTestSuite) TestAcknowledgementAllowedWhenOutboundDisabled() { + suite.SetupTest() + + path := ibctesting.NewPath(suite.chainA, suite.chainB) + suite.coordinator.Setup(path) + + // send packet from A -> B + timeoutHeight := suite.chainB.GetTimeoutHeight() + packet := channeltypes.NewPacket(ibctesting.MockPacketData, 1, + path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, + path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, + timeoutHeight, 0, + ) + err := path.EndpointA.SendPacket(packet) + suite.Require().NoError(err) + + // receive packet on B (creates ack) + err = path.EndpointB.RecvPacket(packet) + suite.Require().NoError(err) + + // disable outbound on chainA + ibcKeeperA := suite.chainA.App.GetIBCKeeper() + ibcKeeperA.SetOutboundEnabled(suite.chainA.GetContext(), false) + suite.Require().False(ibcKeeperA.IsOutboundEnabled(suite.chainA.GetContext())) + + // ack should still succeed + err = path.EndpointA.AcknowledgePacket(packet, ibctesting.MockAcknowledgement) + suite.Require().NoError(err, "MsgAcknowledgement should succeed when outbound is disabled") +} + +// TestTimeoutAllowedWhenOutboundDisabled verifies that MsgTimeout +// succeeds even when OutboundEnabled == false (settlement must be allowed). +func (suite *KeeperTestSuite) TestTimeoutAllowedWhenOutboundDisabled() { + suite.SetupTest() + + path := ibctesting.NewPath(suite.chainA, suite.chainB) + path.SetChannelOrdered() + suite.coordinator.Setup(path) + + // send packet from A -> B with a low timeout height + packet := channeltypes.NewPacket(ibctesting.MockPacketData, 1, + path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelID, + path.EndpointB.ChannelConfig.PortID, path.EndpointB.ChannelID, + clienttypes.GetSelfHeight(suite.chainB.GetContext()), 0, + ) + err := path.EndpointA.SendPacket(packet) + suite.Require().NoError(err) + + // advance chainB past the timeout height + suite.coordinator.CommitNBlocks(suite.chainB, 3) + path.EndpointA.UpdateClient() + + // disable outbound on chainA + ibcKeeperA := suite.chainA.App.GetIBCKeeper() + ibcKeeperA.SetOutboundEnabled(suite.chainA.GetContext(), false) + suite.Require().False(ibcKeeperA.IsOutboundEnabled(suite.chainA.GetContext())) + + // timeout should still succeed + err = path.EndpointA.TimeoutPacket(packet) + suite.Require().NoError(err, "MsgTimeout should succeed when outbound is disabled") +} + +// TestConnectionOpenInit_BlockedWhenOutboundDisabled tests that MsgConnectionOpenInit +// is blocked when outbound IBC is disabled. +func (suite *KeeperTestSuite) TestConnectionOpenInit_BlockedWhenOutboundDisabled() { + suite.SetupTest() + + path := ibctesting.NewPath(suite.chainA, suite.chainB) + suite.coordinator.SetupClients(path) + + // disable outbound on chainA + ibcKeeperA := suite.chainA.App.GetIBCKeeper() + ibcKeeperA.SetOutboundEnabled(suite.chainA.GetContext(), false) + suite.Require().False(ibcKeeperA.IsOutboundEnabled(suite.chainA.GetContext())) + + // craft MsgConnectionOpenInit + msg := connectiontypes.NewMsgConnectionOpenInit( + path.EndpointA.ClientID, path.EndpointB.ClientID, suite.chainB.GetPrefix(), + connectiontypes.DefaultIBCVersion, 0, + suite.chainA.SenderAccount.GetAddress().String(), + ) + + // call the gRPC/keeper ConnectionOpenInit and expect an error + _, err := keeper.Keeper.ConnectionOpenInit(*ibcKeeperA, sdk.WrapSDKContext(suite.chainA.GetContext()), msg) + suite.Require().Error(err, "expected ConnectionOpenInit to return an error when outbound is disabled") + suite.Require().True(strings.Contains(strings.ToLower(err.Error()), "outbound"), + "expected error to mention outbound, got: %s", err.Error()) +} + +// TestChannelOpenInit_BlockedWhenOutboundDisabled tests that MsgChannelOpenInit +// is blocked when outbound IBC is disabled. +func (suite *KeeperTestSuite) TestChannelOpenInit_BlockedWhenOutboundDisabled() { + suite.SetupTest() + + path := ibctesting.NewPath(suite.chainA, suite.chainB) + suite.coordinator.SetupConnections(path) + path.SetChannelOrdered() + + // disable outbound on chainA + ibcKeeperA := suite.chainA.App.GetIBCKeeper() + ibcKeeperA.SetOutboundEnabled(suite.chainA.GetContext(), false) + suite.Require().False(ibcKeeperA.IsOutboundEnabled(suite.chainA.GetContext())) + + // craft MsgChannelOpenInit + msg := channeltypes.NewMsgChannelOpenInit( + path.EndpointA.ChannelConfig.PortID, path.EndpointA.ChannelConfig.Version, + channeltypes.ORDERED, []string{path.EndpointA.ConnectionID}, + ibctesting.MockPort, suite.chainA.SenderAccount.GetAddress().String(), + ) + + // call the gRPC/keeper ChannelOpenInit and expect an error + _, err := keeper.Keeper.ChannelOpenInit(*ibcKeeperA, sdk.WrapSDKContext(suite.chainA.GetContext()), msg) + suite.Require().Error(err, "expected ChannelOpenInit to return an error when outbound is disabled") + suite.Require().True(strings.Contains(strings.ToLower(err.Error()), "outbound"), + "expected error to mention outbound, got: %s", err.Error()) +} diff --git a/sei-ibc-go/modules/core/keeper/keeper.go b/sei-ibc-go/modules/core/keeper/keeper.go index 2d727dc8b2..7743f756b9 100644 --- a/sei-ibc-go/modules/core/keeper/keeper.go +++ b/sei-ibc-go/modules/core/keeper/keeper.go @@ -33,6 +33,8 @@ type Keeper struct { ChannelKeeper channelkeeper.Keeper PortKeeper portkeeper.Keeper Router *porttypes.Router + + paramSpace paramtypes.Subspace } // NewKeeper creates a new ibc Keeper @@ -46,6 +48,8 @@ func NewKeeper( if !paramSpace.HasKeyTable() { keyTable := clienttypes.ParamKeyTable() keyTable.RegisterParamSet(&connectiontypes.Params{}) + // register core params + keyTable.RegisterParamSet(&types.Params{}) paramSpace = paramSpace.WithKeyTable(keyTable) } @@ -65,7 +69,7 @@ func NewKeeper( clientKeeper := clientkeeper.NewKeeper(cdc, key, paramSpace, stakingKeeper, upgradeKeeper) connectionKeeper := connectionkeeper.NewKeeper(cdc, key, paramSpace, clientKeeper) portKeeper := portkeeper.NewKeeper(scopedKeeper) - channelKeeper := channelkeeper.NewKeeper(cdc, key, clientKeeper, connectionKeeper, portKeeper, scopedKeeper) + channelKeeper := channelkeeper.NewKeeper(cdc, key, paramSpace, clientKeeper, connectionKeeper, portKeeper, scopedKeeper) return &Keeper{ cdc: cdc, @@ -73,6 +77,7 @@ func NewKeeper( ConnectionKeeper: connectionKeeper, ChannelKeeper: channelKeeper, PortKeeper: portKeeper, + paramSpace: paramSpace, } } diff --git a/sei-ibc-go/modules/core/keeper/msg_server.go b/sei-ibc-go/modules/core/keeper/msg_server.go index d51dbef1ee..0245045557 100644 --- a/sei-ibc-go/modules/core/keeper/msg_server.go +++ b/sei-ibc-go/modules/core/keeper/msg_server.go @@ -100,6 +100,11 @@ func (k Keeper) SubmitMisbehaviour(goCtx context.Context, msg *clienttypes.MsgSu func (k Keeper) ConnectionOpenInit(goCtx context.Context, msg *connectiontypes.MsgConnectionOpenInit) (*connectiontypes.MsgConnectionOpenInitResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + // outbound gating: disallow outbound connection inits when outbound disabled + if !k.IsOutboundEnabled(ctx) { + return nil, sdkerrors.Wrap(coretypes.ErrOutboundDisabled, "connection outbound disabled") + } + if _, err := k.ConnectionKeeper.ConnOpenInit(ctx, msg.ClientId, msg.Counterparty, msg.Version, msg.DelayPeriod); err != nil { return nil, sdkerrors.Wrap(err, "connection handshake open init failed") } @@ -111,6 +116,10 @@ func (k Keeper) ConnectionOpenInit(goCtx context.Context, msg *connectiontypes.M func (k Keeper) ConnectionOpenTry(goCtx context.Context, msg *connectiontypes.MsgConnectionOpenTry) (*connectiontypes.MsgConnectionOpenTryResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + if !k.IsInboundEnabled(ctx) { + return nil, sdkerrors.Wrap(coretypes.ErrInboundDisabled, "connection inbound disabled") + } + targetClient, err := clienttypes.UnpackClientState(msg.ClientState) if err != nil { return nil, err @@ -165,6 +174,11 @@ func (k Keeper) ConnectionOpenConfirm(goCtx context.Context, msg *connectiontype func (k Keeper) ChannelOpenInit(goCtx context.Context, msg *channeltypes.MsgChannelOpenInit) (*channeltypes.MsgChannelOpenInitResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + // outbound gating: disallow outbound channel inits when outbound disabled + if !k.IsOutboundEnabled(ctx) { + return nil, sdkerrors.Wrap(coretypes.ErrOutboundDisabled, "channel outbound disabled") + } + // Lookup module by port capability module, portCap, err := k.PortKeeper.LookupModuleByPort(ctx, msg.PortId) if err != nil { @@ -206,6 +220,10 @@ func (k Keeper) ChannelOpenInit(goCtx context.Context, msg *channeltypes.MsgChan func (k Keeper) ChannelOpenTry(goCtx context.Context, msg *channeltypes.MsgChannelOpenTry) (*channeltypes.MsgChannelOpenTryResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + if !k.IsInboundEnabled(ctx) { + return nil, sdkerrors.Wrap(coretypes.ErrInboundDisabled, "channel inbound disabled") + } + // Lookup module by port capability module, portCap, err := k.PortKeeper.LookupModuleByPort(ctx, msg.PortId) if err != nil { @@ -369,6 +387,10 @@ func (k Keeper) ChannelCloseConfirm(goCtx context.Context, msg *channeltypes.Msg func (k Keeper) RecvPacket(goCtx context.Context, msg *channeltypes.MsgRecvPacket) (*channeltypes.MsgRecvPacketResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + if !k.IsInboundEnabled(ctx) { + return nil, sdkerrors.Wrap(coretypes.ErrInboundDisabled, "recv packet disabled") + } + relayer, err := sdk.AccAddressFromBech32(msg.Signer) if err != nil { return nil, sdkerrors.Wrap(err, "Invalid address for msg Signer") diff --git a/sei-ibc-go/modules/core/keeper/params.go b/sei-ibc-go/modules/core/keeper/params.go new file mode 100644 index 0000000000..a1ed9ee6af --- /dev/null +++ b/sei-ibc-go/modules/core/keeper/params.go @@ -0,0 +1,49 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" + + "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/core/types" +) + +// GetParams returns the total set of ibc core module parameters. +func (k *Keeper) GetParams(ctx sdk.Context) types.Params { + var p types.Params + k.paramSpace.GetParamSet(ctx, &p) + return p +} + +// SetParams sets the ibc core module parameters. +func (k *Keeper) SetParams(ctx sdk.Context, p types.Params) { + k.paramSpace.SetParamSet(ctx, &p) +} + +// IsInboundEnabled returns true if inbound IBC is enabled. +func (k *Keeper) IsInboundEnabled(ctx sdk.Context) bool { + return k.GetParams(ctx).InboundEnabled +} + +// IsOutboundEnabled returns true if outbound IBC is enabled. +func (k *Keeper) IsOutboundEnabled(ctx sdk.Context) bool { + return k.GetParams(ctx).OutboundEnabled +} + +// SetInboundEnabled sets inbound enabled flag. +func (k *Keeper) SetInboundEnabled(ctx sdk.Context, enabled bool) { + p := k.GetParams(ctx) + p.InboundEnabled = enabled + k.SetParams(ctx, p) +} + +// SetOutboundEnabled sets outbound enabled flag. +func (k *Keeper) SetOutboundEnabled(ctx sdk.Context, enabled bool) { + p := k.GetParams(ctx) + p.OutboundEnabled = enabled + k.SetParams(ctx, p) +} + +// GetParamSpace returns the keeper's paramSpace (for other packages if needed). +func (k *Keeper) GetParamSpace() paramtypes.Subspace { + return k.paramSpace +} diff --git a/sei-ibc-go/modules/core/keeper/params_test.go b/sei-ibc-go/modules/core/keeper/params_test.go new file mode 100644 index 0000000000..fb23fe9834 --- /dev/null +++ b/sei-ibc-go/modules/core/keeper/params_test.go @@ -0,0 +1,29 @@ +package keeper_test + +import ( + "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/core/types" +) + +func (suite *KeeperTestSuite) TestCoreParams_GetSet() { + ctx := suite.chainA.GetContext() + ik := suite.chainA.App.GetIBCKeeper() + + // default params should be true,true + params := ik.GetParams(ctx) + suite.Require().True(params.InboundEnabled) + suite.Require().True(params.OutboundEnabled) + + // toggle inbound -> false + ik.SetInboundEnabled(ctx, false) + suite.Require().False(ik.IsInboundEnabled(ctx)) + + // toggle outbound -> false + ik.SetOutboundEnabled(ctx, false) + suite.Require().False(ik.IsOutboundEnabled(ctx)) + + // restore defaults + ik.SetParams(ctx, types.DefaultParams()) + params = ik.GetParams(ctx) + suite.Require().True(params.InboundEnabled) + suite.Require().True(params.OutboundEnabled) +} diff --git a/sei-ibc-go/modules/core/types/errors.go b/sei-ibc-go/modules/core/types/errors.go new file mode 100644 index 0000000000..dfc94b87dc --- /dev/null +++ b/sei-ibc-go/modules/core/types/errors.go @@ -0,0 +1,11 @@ +package types + +import ( + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +var ( + // ErrInboundDisabled / ErrOutboundDisabled + ErrInboundDisabled = sdkerrors.Register("ibc", 101, "ibc inbound disabled") + ErrOutboundDisabled = sdkerrors.Register("ibc", 102, "ibc outbound disabled") +) diff --git a/sei-ibc-go/modules/core/types/params.go b/sei-ibc-go/modules/core/types/params.go new file mode 100644 index 0000000000..bab4d1bb25 --- /dev/null +++ b/sei-ibc-go/modules/core/types/params.go @@ -0,0 +1,58 @@ +package types + +import ( + "fmt" + + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" +) + +var ( + KeyInboundEnabled = []byte("InboundEnabled") + KeyOutboundEnabled = []byte("OutboundEnabled") +) + +type Params struct { + InboundEnabled bool `json:"inbound_enabled" yaml:"inbound_enabled"` + OutboundEnabled bool `json:"outbound_enabled" yaml:"outbound_enabled"` +} + +// ParamKeyTable for the ibc core module params +func ParamKeyTable() paramtypes.KeyTable { + return paramtypes.NewKeyTable().RegisterParamSet(&Params{}) +} + +func NewParams(inbound, outbound bool) Params { + return Params{ + InboundEnabled: inbound, + OutboundEnabled: outbound, + } +} + +func DefaultParams() Params { + return NewParams(true, true) +} + +func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { + return paramtypes.ParamSetPairs{ + paramtypes.NewParamSetPair(KeyInboundEnabled, &p.InboundEnabled, validateBool), + paramtypes.NewParamSetPair(KeyOutboundEnabled, &p.OutboundEnabled, validateBool), + } +} + +func (p Params) Validate() error { + if err := validateBool(p.InboundEnabled); err != nil { + return fmt.Errorf("inbound_enabled: %w", err) + } + if err := validateBool(p.OutboundEnabled); err != nil { + return fmt.Errorf("outbound_enabled: %w", err) + } + return nil +} + +func validateBool(i interface{}) error { + _, ok := i.(bool) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + return nil +}