From 4c8ece33c33249d2b12f763decbb3012ef0ef387 Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Wed, 31 Dec 2025 12:26:47 -0500 Subject: [PATCH 1/8] IBC toggles --- .../modules/core/04-channel/keeper/keeper.go | 15 ++++- .../modules/core/04-channel/keeper/packet.go | 8 +++ .../modules/core/keeper/inbound_test.go | 60 +++++++++++++++++++ sei-ibc-go/modules/core/keeper/keeper.go | 7 ++- sei-ibc-go/modules/core/keeper/msg_server.go | 12 ++++ sei-ibc-go/modules/core/keeper/params.go | 50 ++++++++++++++++ sei-ibc-go/modules/core/keeper/params_test.go | 30 ++++++++++ sei-ibc-go/modules/core/types/errors.go | 12 ++++ sei-ibc-go/modules/core/types/params.go | 59 ++++++++++++++++++ 9 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 sei-ibc-go/modules/core/keeper/inbound_test.go create mode 100644 sei-ibc-go/modules/core/keeper/params.go create mode 100644 sei-ibc-go/modules/core/keeper/params_test.go create mode 100644 sei-ibc-go/modules/core/types/errors.go create mode 100644 sei-ibc-go/modules/core/types/params.go 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..2174a049ef 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,16 @@ func NewKeeper( } } +// KeyOutboundEnabled is the param key for outbound enabled (duplicated from core/types to avoid import cycle) +var KeyOutboundEnabled = []byte("OutboundEnabled") + +// 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 +} + // 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..3964bf521c 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,9 @@ import ( "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/core/exported" ) +// ErrOutboundDisabled is the error for when outbound is disabled (duplicated from core/types to avoid import cycle) +var ErrOutboundDisabled = sdkerrors.Register("ibc-channel", 102, "ibc outbound 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 +26,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") } 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..51fe63cbf7 --- /dev/null +++ b/sei-ibc-go/modules/core/keeper/inbound_test.go @@ -0,0 +1,60 @@ +package keeper_test + +import ( + "strings" + + sdk "github.com/cosmos/cosmos-sdk/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()) +} + 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..11b5f6f3e3 100644 --- a/sei-ibc-go/modules/core/keeper/msg_server.go +++ b/sei-ibc-go/modules/core/keeper/msg_server.go @@ -111,6 +111,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 @@ -206,6 +210,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 +377,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..10957a8479 --- /dev/null +++ b/sei-ibc-go/modules/core/keeper/params.go @@ -0,0 +1,50 @@ +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..524ea524fd --- /dev/null +++ b/sei-ibc-go/modules/core/keeper/params_test.go @@ -0,0 +1,30 @@ +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..76bf79dc8f --- /dev/null +++ b/sei-ibc-go/modules/core/types/errors.go @@ -0,0 +1,12 @@ +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..be1023626d --- /dev/null +++ b/sei-ibc-go/modules/core/types/params.go @@ -0,0 +1,59 @@ +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 +} + From 932f074f130fd5e2391806d376afa64b1eeb8c4e Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Wed, 31 Dec 2025 12:42:07 -0500 Subject: [PATCH 2/8] Update genesis --- sei-ibc-go/modules/core/genesis.go | 3 +++ sei-ibc-go/modules/core/types/params.go | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) 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/types/params.go b/sei-ibc-go/modules/core/types/params.go index be1023626d..bab4d1bb25 100644 --- a/sei-ibc-go/modules/core/types/params.go +++ b/sei-ibc-go/modules/core/types/params.go @@ -56,4 +56,3 @@ func validateBool(i interface{}) error { } return nil } - From d7f5f741741101b6632a11e7a922068f3df2e817 Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Wed, 31 Dec 2025 12:46:22 -0500 Subject: [PATCH 3/8] Outbound gate --- sei-ibc-go/modules/core/keeper/msg_server.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/sei-ibc-go/modules/core/keeper/msg_server.go b/sei-ibc-go/modules/core/keeper/msg_server.go index 11b5f6f3e3..644a92755c 100644 --- a/sei-ibc-go/modules/core/keeper/msg_server.go +++ b/sei-ibc-go/modules/core/keeper/msg_server.go @@ -459,6 +459,10 @@ func (k Keeper) RecvPacket(goCtx context.Context, msg *channeltypes.MsgRecvPacke func (k Keeper) Timeout(goCtx context.Context, msg *channeltypes.MsgTimeout) (*channeltypes.MsgTimeoutResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + if !k.IsOutboundEnabled(ctx) { + return nil, sdkerrors.Wrap(coretypes.ErrOutboundDisabled, "timeout processing disabled") + } + relayer, err := sdk.AccAddressFromBech32(msg.Signer) if err != nil { return nil, sdkerrors.Wrap(err, "Invalid address for msg Signer") @@ -527,6 +531,10 @@ func (k Keeper) Timeout(goCtx context.Context, msg *channeltypes.MsgTimeout) (*c func (k Keeper) TimeoutOnClose(goCtx context.Context, msg *channeltypes.MsgTimeoutOnClose) (*channeltypes.MsgTimeoutOnCloseResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + if !k.IsOutboundEnabled(ctx) { + return nil, sdkerrors.Wrap(coretypes.ErrOutboundDisabled, "timeout on close processing disabled") + } + relayer, err := sdk.AccAddressFromBech32(msg.Signer) if err != nil { return nil, sdkerrors.Wrap(err, "Invalid address for msg Signer") @@ -598,6 +606,10 @@ func (k Keeper) TimeoutOnClose(goCtx context.Context, msg *channeltypes.MsgTimeo func (k Keeper) Acknowledgement(goCtx context.Context, msg *channeltypes.MsgAcknowledgement) (*channeltypes.MsgAcknowledgementResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + if !k.IsOutboundEnabled(ctx) { + return nil, sdkerrors.Wrap(coretypes.ErrOutboundDisabled, "acknowledgement processing disabled") + } + relayer, err := sdk.AccAddressFromBech32(msg.Signer) if err != nil { return nil, sdkerrors.Wrap(err, "Invalid address for msg Signer") From 11f4d4c5c74e086f0fe4f3c3c16752d90893a4ed Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Fri, 2 Jan 2026 13:51:35 -0500 Subject: [PATCH 4/8] Do not gate Acks and timeouts --- sei-ibc-go/modules/core/keeper/msg_server.go | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/sei-ibc-go/modules/core/keeper/msg_server.go b/sei-ibc-go/modules/core/keeper/msg_server.go index 644a92755c..11b5f6f3e3 100644 --- a/sei-ibc-go/modules/core/keeper/msg_server.go +++ b/sei-ibc-go/modules/core/keeper/msg_server.go @@ -459,10 +459,6 @@ func (k Keeper) RecvPacket(goCtx context.Context, msg *channeltypes.MsgRecvPacke func (k Keeper) Timeout(goCtx context.Context, msg *channeltypes.MsgTimeout) (*channeltypes.MsgTimeoutResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - if !k.IsOutboundEnabled(ctx) { - return nil, sdkerrors.Wrap(coretypes.ErrOutboundDisabled, "timeout processing disabled") - } - relayer, err := sdk.AccAddressFromBech32(msg.Signer) if err != nil { return nil, sdkerrors.Wrap(err, "Invalid address for msg Signer") @@ -531,10 +527,6 @@ func (k Keeper) Timeout(goCtx context.Context, msg *channeltypes.MsgTimeout) (*c func (k Keeper) TimeoutOnClose(goCtx context.Context, msg *channeltypes.MsgTimeoutOnClose) (*channeltypes.MsgTimeoutOnCloseResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - if !k.IsOutboundEnabled(ctx) { - return nil, sdkerrors.Wrap(coretypes.ErrOutboundDisabled, "timeout on close processing disabled") - } - relayer, err := sdk.AccAddressFromBech32(msg.Signer) if err != nil { return nil, sdkerrors.Wrap(err, "Invalid address for msg Signer") @@ -606,10 +598,6 @@ func (k Keeper) TimeoutOnClose(goCtx context.Context, msg *channeltypes.MsgTimeo func (k Keeper) Acknowledgement(goCtx context.Context, msg *channeltypes.MsgAcknowledgement) (*channeltypes.MsgAcknowledgementResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - if !k.IsOutboundEnabled(ctx) { - return nil, sdkerrors.Wrap(coretypes.ErrOutboundDisabled, "acknowledgement processing disabled") - } - relayer, err := sdk.AccAddressFromBech32(msg.Signer) if err != nil { return nil, sdkerrors.Wrap(err, "Invalid address for msg Signer") From 1f99542f6649ba450a6357875467696b80ae1ef9 Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Fri, 2 Jan 2026 14:55:09 -0500 Subject: [PATCH 5/8] Deeper keeper checks + unit tests --- .../modules/core/02-client/keeper/client.go | 8 ++++ .../core/02-client/keeper/client_test.go | 21 ++++++++ .../modules/core/02-client/keeper/keeper.go | 10 ++++ .../core/03-connection/keeper/handshake.go | 5 ++ .../03-connection/keeper/handshake_test.go | 48 +++++++++++++++++++ .../core/03-connection/keeper/keeper.go | 13 +++++ .../core/04-channel/keeper/handshake.go | 8 ++++ .../core/04-channel/keeper/handshake_test.go | 43 +++++++++++++++++ .../modules/core/04-channel/keeper/keeper.go | 10 ++++ .../modules/core/04-channel/keeper/packet.go | 8 ++++ .../core/04-channel/keeper/packet_test.go | 40 ++++++++++++++++ 11 files changed, 214 insertions(+) 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..4dc6b23d9a 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 (duplicated from core/types to avoid import cycle) +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..94ab723c95 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 (duplicated from core/types to avoid import cycle) +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..0b2c7be658 100644 --- a/sei-ibc-go/modules/core/03-connection/keeper/handshake.go +++ b/sei-ibc-go/modules/core/03-connection/keeper/handshake.go @@ -75,6 +75,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..563ec33661 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,54 @@ 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") +} + // 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..d8a4c8313a 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,12 @@ import ( "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/core/exported" ) +// KeyInboundEnabled is the param key for inbound enabled (duplicated from core/types to avoid import cycle) +var KeyInboundEnabled = []byte("InboundEnabled") + +// ErrInboundDisabled is the error for when inbound is disabled (duplicated from core/types to avoid import cycle) +var ErrInboundDisabled = sdkerrors.Register("ibc-connection", 101, "ibc inbound disabled") + // Keeper defines the IBC connection keeper type Keeper struct { // implements gRPC QueryServer interface @@ -46,6 +52,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 +} + // 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..f4f94b64b8 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,9 @@ 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") + // 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. @@ -106,6 +109,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..2439c3d03c 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,49 @@ 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") +} + // 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 2174a049ef..64db8a232b 100644 --- a/sei-ibc-go/modules/core/04-channel/keeper/keeper.go +++ b/sei-ibc-go/modules/core/04-channel/keeper/keeper.go @@ -57,6 +57,9 @@ func NewKeeper( // KeyOutboundEnabled is the param key for outbound enabled (duplicated from core/types to avoid import cycle) var KeyOutboundEnabled = []byte("OutboundEnabled") +// KeyInboundEnabled is the param key for inbound enabled (duplicated from core/types to avoid import cycle) +var KeyInboundEnabled = []byte("InboundEnabled") + // IsOutboundEnabled returns true if outbound IBC is enabled. func (k Keeper) IsOutboundEnabled(ctx sdk.Context) bool { var outbound bool @@ -64,6 +67,13 @@ func (k Keeper) IsOutboundEnabled(ctx sdk.Context) bool { 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 3964bf521c..7ac17aac50 100644 --- a/sei-ibc-go/modules/core/04-channel/keeper/packet.go +++ b/sei-ibc-go/modules/core/04-channel/keeper/packet.go @@ -18,6 +18,9 @@ import ( // ErrOutboundDisabled is the error for when outbound is disabled (duplicated from core/types to avoid import cycle) var ErrOutboundDisabled = sdkerrors.Register("ibc-channel", 102, "ibc outbound disabled") +// ErrInboundDisabled is the error for when inbound is disabled (duplicated from core/types to avoid import cycle) +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. @@ -165,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 From fcbcb5c97d0c8b3ffa823203aa4340f515e5b042 Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Fri, 2 Jan 2026 14:57:38 -0500 Subject: [PATCH 6/8] Update comment --- sei-ibc-go/modules/core/02-client/keeper/client.go | 2 +- sei-ibc-go/modules/core/02-client/keeper/keeper.go | 2 +- sei-ibc-go/modules/core/03-connection/keeper/keeper.go | 4 ++-- sei-ibc-go/modules/core/04-channel/keeper/keeper.go | 4 ++-- sei-ibc-go/modules/core/04-channel/keeper/packet.go | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) 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 4dc6b23d9a..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,7 +12,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/core/exported" ) -// ErrInboundDisabled is the error for when inbound is disabled (duplicated from core/types to avoid import cycle) +// 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 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 94ab723c95..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,7 +22,7 @@ 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 (duplicated from core/types to avoid import cycle) +// 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 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 d8a4c8313a..a6dd02ba01 100644 --- a/sei-ibc-go/modules/core/03-connection/keeper/keeper.go +++ b/sei-ibc-go/modules/core/03-connection/keeper/keeper.go @@ -15,10 +15,10 @@ import ( "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/core/exported" ) -// KeyInboundEnabled is the param key for inbound enabled (duplicated from core/types to avoid import cycle) +// KeyInboundEnabled is the param key for inbound enabled var KeyInboundEnabled = []byte("InboundEnabled") -// ErrInboundDisabled is the error for when inbound is disabled (duplicated from core/types to avoid import cycle) +// ErrInboundDisabled is the error for when inbound is disabled var ErrInboundDisabled = sdkerrors.Register("ibc-connection", 101, "ibc inbound disabled") // Keeper defines the IBC connection keeper 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 64db8a232b..4dc0069bb4 100644 --- a/sei-ibc-go/modules/core/04-channel/keeper/keeper.go +++ b/sei-ibc-go/modules/core/04-channel/keeper/keeper.go @@ -54,10 +54,10 @@ func NewKeeper( } } -// KeyOutboundEnabled is the param key for outbound enabled (duplicated from core/types to avoid import cycle) +// KeyOutboundEnabled is the param key for outbound enabled var KeyOutboundEnabled = []byte("OutboundEnabled") -// KeyInboundEnabled is the param key for inbound enabled (duplicated from core/types to avoid import cycle) +// KeyInboundEnabled is the param key for inbound enabled var KeyInboundEnabled = []byte("InboundEnabled") // IsOutboundEnabled returns true if outbound IBC is enabled. 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 7ac17aac50..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,10 +15,10 @@ import ( "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/core/exported" ) -// ErrOutboundDisabled is the error for when outbound is disabled (duplicated from core/types to avoid import cycle) +// 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 (duplicated from core/types to avoid import cycle) +// 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 From 77c6a7db9a988b977e0c9fb2ba52859215a7acd1 Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Fri, 2 Jan 2026 14:59:07 -0500 Subject: [PATCH 7/8] lint --- sei-ibc-go/modules/core/keeper/inbound_test.go | 1 - sei-ibc-go/modules/core/keeper/params.go | 1 - sei-ibc-go/modules/core/keeper/params_test.go | 1 - sei-ibc-go/modules/core/types/errors.go | 1 - 4 files changed, 4 deletions(-) diff --git a/sei-ibc-go/modules/core/keeper/inbound_test.go b/sei-ibc-go/modules/core/keeper/inbound_test.go index 51fe63cbf7..d4b617ec96 100644 --- a/sei-ibc-go/modules/core/keeper/inbound_test.go +++ b/sei-ibc-go/modules/core/keeper/inbound_test.go @@ -57,4 +57,3 @@ func (suite *KeeperTestSuite) TestRecvPacket_BlockedWhenInboundDisabled() { suite.Require().True(strings.Contains(strings.ToLower(err.Error()), "inbound"), "expected error to mention inbound/disabled, got: %s", err.Error()) } - diff --git a/sei-ibc-go/modules/core/keeper/params.go b/sei-ibc-go/modules/core/keeper/params.go index 10957a8479..a1ed9ee6af 100644 --- a/sei-ibc-go/modules/core/keeper/params.go +++ b/sei-ibc-go/modules/core/keeper/params.go @@ -47,4 +47,3 @@ func (k *Keeper) SetOutboundEnabled(ctx sdk.Context, enabled bool) { 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 index 524ea524fd..fb23fe9834 100644 --- a/sei-ibc-go/modules/core/keeper/params_test.go +++ b/sei-ibc-go/modules/core/keeper/params_test.go @@ -27,4 +27,3 @@ func (suite *KeeperTestSuite) TestCoreParams_GetSet() { 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 index 76bf79dc8f..dfc94b87dc 100644 --- a/sei-ibc-go/modules/core/types/errors.go +++ b/sei-ibc-go/modules/core/types/errors.go @@ -9,4 +9,3 @@ var ( ErrInboundDisabled = sdkerrors.Register("ibc", 101, "ibc inbound disabled") ErrOutboundDisabled = sdkerrors.Register("ibc", 102, "ibc outbound disabled") ) - From a997036a8b3982a9ccf4bdc2e0b09e03ddc9a422 Mon Sep 17 00:00:00 2001 From: kbhat1 Date: Fri, 2 Jan 2026 15:32:05 -0500 Subject: [PATCH 8/8] Update ack + timeout unit tests --- .../modules/core/keeper/inbound_test.go | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/sei-ibc-go/modules/core/keeper/inbound_test.go b/sei-ibc-go/modules/core/keeper/inbound_test.go index d4b617ec96..08892d573f 100644 --- a/sei-ibc-go/modules/core/keeper/inbound_test.go +++ b/sei-ibc-go/modules/core/keeper/inbound_test.go @@ -5,6 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" + clienttypes "github.com/sei-protocol/sei-chain/sei-ibc-go/modules/core/02-client/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" @@ -57,3 +58,67 @@ func (suite *KeeperTestSuite) TestRecvPacket_BlockedWhenInboundDisabled() { 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") +}