Some checks failed
CodeQL / Analyze (push) Waiting to run
golangci-lint / lint (push) Waiting to run
Tests / Code Coverage / build (amd64) (push) Waiting to run
Tests / Code Coverage / build (arm64) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[additional-args:-tags="test_e2e" name:e2e path:./e2e]) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[name:08-wasm path:./modules/light-clients/08-wasm]) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[name:ibc-go path:.]) (push) Waiting to run
Docker Build & Push Simapp (main) / docker-build (push) Has been cancelled
848 lines
24 KiB
Go
848 lines
24 KiB
Go
package v2_test
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
errorsmod "cosmossdk.io/errors"
|
|
storetypes "cosmossdk.io/store/types"
|
|
|
|
sdk "git.cw.tr/mukan-network/mukan-sdk/types"
|
|
|
|
"git.cw.tr/mukan-network/mukan-ibc/modules/apps/callbacks/testing/simapp"
|
|
"git.cw.tr/mukan-network/mukan-ibc/modules/apps/callbacks/types"
|
|
v2 "git.cw.tr/mukan-network/mukan-ibc/modules/apps/callbacks/v2"
|
|
transfertypes "git.cw.tr/mukan-network/mukan-ibc/modules/apps/transfer/types"
|
|
channeltypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/04-channel/types"
|
|
channelkeeperv2 "git.cw.tr/mukan-network/mukan-ibc/modules/core/04-channel/v2/keeper"
|
|
channeltypesv2 "git.cw.tr/mukan-network/mukan-ibc/modules/core/04-channel/v2/types"
|
|
"git.cw.tr/mukan-network/mukan-ibc/modules/core/api"
|
|
ibcerrors "git.cw.tr/mukan-network/mukan-ibc/modules/core/errors"
|
|
ibctesting "git.cw.tr/mukan-network/mukan-ibc/testing"
|
|
ibcmock "git.cw.tr/mukan-network/mukan-ibc/testing/mock"
|
|
ibcmockv2 "git.cw.tr/mukan-network/mukan-ibc/testing/mock/v2"
|
|
)
|
|
|
|
func (s *CallbacksTestSuite) TestNewIBCMiddleware() {
|
|
testCases := []struct {
|
|
name string
|
|
instantiateFn func()
|
|
expError error
|
|
}{
|
|
{
|
|
"success",
|
|
func() {
|
|
_ = v2.NewIBCMiddleware(ibcmockv2.IBCModule{}, &channelkeeperv2.Keeper{}, simapp.ContractKeeper{}, &channelkeeperv2.Keeper{}, maxCallbackGas)
|
|
},
|
|
nil,
|
|
},
|
|
{
|
|
"panics with nil ics4wrapper",
|
|
func() {
|
|
_ = v2.NewIBCMiddleware(ibcmockv2.IBCModule{}, nil, simapp.ContractKeeper{}, &channelkeeperv2.Keeper{}, maxCallbackGas)
|
|
},
|
|
errors.New("write acknowledgement wrapper cannot be nil"),
|
|
},
|
|
{
|
|
"panics with nil underlying app",
|
|
func() {
|
|
_ = v2.NewIBCMiddleware(nil, &channelkeeperv2.Keeper{}, simapp.ContractKeeper{}, &channelkeeperv2.Keeper{}, maxCallbackGas)
|
|
},
|
|
fmt.Errorf("underlying application does not implement %T", (*types.CallbacksCompatibleModule)(nil)),
|
|
},
|
|
{
|
|
"panics with nil contract keeper",
|
|
func() {
|
|
_ = v2.NewIBCMiddleware(ibcmockv2.IBCModule{}, &channelkeeperv2.Keeper{}, nil, &channelkeeperv2.Keeper{}, maxCallbackGas)
|
|
},
|
|
errors.New("contract keeper cannot be nil"),
|
|
},
|
|
{
|
|
"panics with nil channel v2 keeper",
|
|
func() {
|
|
_ = v2.NewIBCMiddleware(ibcmockv2.IBCModule{}, &channelkeeperv2.Keeper{}, simapp.ContractKeeper{}, nil, maxCallbackGas)
|
|
},
|
|
errors.New("channel keeper v2 cannot be nil"),
|
|
},
|
|
{
|
|
"panics with zero maxCallbackGas",
|
|
func() {
|
|
_ = v2.NewIBCMiddleware(ibcmockv2.IBCModule{}, &channelkeeperv2.Keeper{}, simapp.ContractKeeper{}, &channelkeeperv2.Keeper{}, uint64(0))
|
|
},
|
|
errors.New("maxCallbackGas cannot be zero"),
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
s.Run(tc.name, func() {
|
|
if tc.expError == nil {
|
|
s.Require().NotPanics(tc.instantiateFn, "unexpected panic: NewIBCMiddleware")
|
|
} else {
|
|
s.Require().PanicsWithError(tc.expError.Error(), tc.instantiateFn, "expected panic with error: ", tc.expError.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *CallbacksTestSuite) TestWithWriteAckWrapper() {
|
|
s.setupChains()
|
|
|
|
cbsMiddleware := v2.IBCMiddleware{}
|
|
s.Require().Nil(cbsMiddleware.GetWriteAckWrapper())
|
|
|
|
cbsMiddleware.WithWriteAckWrapper(s.chainA.App.GetIBCKeeper().ChannelKeeperV2)
|
|
writeAckWrapper := cbsMiddleware.GetWriteAckWrapper()
|
|
|
|
s.Require().IsType((*channelkeeperv2.Keeper)(nil), writeAckWrapper)
|
|
}
|
|
|
|
func (s *CallbacksTestSuite) TestSendPacket() {
|
|
var packetData transfertypes.FungibleTokenPacketData
|
|
|
|
testCases := []struct {
|
|
name string
|
|
malleate func()
|
|
callbackType types.CallbackType
|
|
expPanic bool
|
|
expValue any
|
|
}{
|
|
{
|
|
"success",
|
|
func() {},
|
|
types.CallbackTypeSendPacket,
|
|
false,
|
|
nil,
|
|
},
|
|
{
|
|
"success: callback data nonexistent",
|
|
func() {
|
|
//nolint:goconst
|
|
packetData.Memo = ""
|
|
},
|
|
"none",
|
|
false,
|
|
nil,
|
|
},
|
|
{
|
|
"failure: no-op on callback data is not valid",
|
|
func() {
|
|
//nolint:goconst
|
|
packetData.Memo = `{"src_callback": {"address": ""}}`
|
|
},
|
|
"none", // improperly formatted callback data should result in no callback execution
|
|
false,
|
|
types.ErrInvalidCallbackData,
|
|
},
|
|
{
|
|
"failure: callback execution fails",
|
|
func() {
|
|
packetData.Memo = fmt.Sprintf(`{"src_callback": {"address":"%s"}}`, simapp.ErrorContract)
|
|
},
|
|
types.CallbackTypeSendPacket,
|
|
false,
|
|
ibcmock.MockApplicationCallbackError, // execution failure on SendPacket should prevent packet sends
|
|
},
|
|
{
|
|
"failure: callback execution reach out of gas panic, but sufficient gas provided",
|
|
func() {
|
|
packetData.Memo = fmt.Sprintf(`{"src_callback": {"address":"%s", "gas_limit":"400000"}}`, simapp.OogPanicContract)
|
|
},
|
|
types.CallbackTypeSendPacket,
|
|
true,
|
|
storetypes.ErrorOutOfGas{Descriptor: fmt.Sprintf("mock %s callback oog panic", types.CallbackTypeSendPacket)},
|
|
},
|
|
{
|
|
"failure: callback execution reach out of gas error, but sufficient gas provided",
|
|
func() {
|
|
packetData.Memo = fmt.Sprintf(`{"src_callback": {"address":"%s", "gas_limit":"400000"}}`, simapp.OogErrorContract)
|
|
},
|
|
types.CallbackTypeSendPacket,
|
|
false,
|
|
errorsmod.Wrapf(types.ErrCallbackOutOfGas, "ibc %s callback out of gas", types.CallbackTypeSendPacket),
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
s.Run(tc.name, func() {
|
|
s.SetupTest()
|
|
|
|
packetData = transfertypes.NewFungibleTokenPacketData(
|
|
ibctesting.TestCoin.Denom,
|
|
ibctesting.TestCoin.Amount.String(),
|
|
s.chainA.SenderAccount.GetAddress().String(),
|
|
ibctesting.TestAccAddress,
|
|
fmt.Sprintf(`{"src_callback": {"address": "%s"}}`, simapp.SuccessContract),
|
|
)
|
|
|
|
tc.malleate()
|
|
|
|
payload := channeltypesv2.NewPayload(
|
|
transfertypes.PortID, transfertypes.PortID,
|
|
transfertypes.V1, transfertypes.EncodingJSON,
|
|
packetData.GetBytes(),
|
|
)
|
|
|
|
ctx := s.chainA.GetContext()
|
|
gasLimit := ctx.GasMeter().Limit()
|
|
|
|
var err error
|
|
sendPacket := func() {
|
|
cbs := s.chainA.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort)
|
|
|
|
err = cbs.OnSendPacket(ctx, s.path.EndpointA.ClientID, s.path.EndpointB.ClientID,
|
|
1, payload, s.chainA.SenderAccount.GetAddress())
|
|
}
|
|
|
|
expPass := tc.expValue == nil
|
|
switch {
|
|
case expPass:
|
|
sendPacket()
|
|
s.Require().Nil(err)
|
|
|
|
expEvent, exists := GetExpectedEvent(
|
|
ctx, packetData, gasLimit, payload.Version,
|
|
transfertypes.PortID, s.path.EndpointA.ClientID, 1, types.CallbackTypeSendPacket, nil,
|
|
)
|
|
if exists {
|
|
s.Require().Contains(ctx.EventManager().Events().ToABCIEvents(), expEvent)
|
|
}
|
|
|
|
case tc.expPanic:
|
|
s.Require().PanicsWithValue(tc.expValue, sendPacket)
|
|
|
|
default:
|
|
sendPacket()
|
|
s.Require().ErrorIs(err, tc.expValue.(error))
|
|
}
|
|
|
|
s.AssertHasExecutedExpectedCallback(tc.callbackType, expPass)
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *CallbacksTestSuite) TestOnAcknowledgementPacket() {
|
|
type expResult uint8
|
|
const (
|
|
noExecution expResult = iota
|
|
callbackFailed
|
|
callbackSuccess
|
|
)
|
|
|
|
var (
|
|
packetData transfertypes.FungibleTokenPacketData
|
|
ack []byte
|
|
ctx sdk.Context
|
|
userGasLimit uint64
|
|
)
|
|
|
|
panicError := errors.New("panic error")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
malleate func()
|
|
expResult expResult
|
|
expError error
|
|
}{
|
|
{
|
|
"success",
|
|
func() {},
|
|
callbackSuccess,
|
|
nil,
|
|
},
|
|
{
|
|
"success: callback data nonexistent",
|
|
func() {
|
|
//nolint:goconst
|
|
packetData.Memo = ""
|
|
},
|
|
noExecution,
|
|
nil,
|
|
},
|
|
{
|
|
"failure: underlying app OnAcknowledgePacket fails",
|
|
func() {
|
|
ack = []byte("invalid ack")
|
|
},
|
|
noExecution,
|
|
ibcerrors.ErrUnknownRequest,
|
|
},
|
|
{
|
|
"failure: callback data is not valid",
|
|
func() {
|
|
//nolint:goconst
|
|
packetData.Memo = `{"src_callback": {"address": ""}}`
|
|
},
|
|
noExecution,
|
|
types.ErrInvalidCallbackData,
|
|
},
|
|
{
|
|
"failure: callback execution reach out of gas, but sufficient gas provided by relayer",
|
|
func() {
|
|
packetData.Memo = fmt.Sprintf(`{"src_callback": {"address":"%s", "gas_limit":"%d"}}`, simapp.OogPanicContract, userGasLimit)
|
|
},
|
|
callbackFailed,
|
|
nil,
|
|
},
|
|
{
|
|
"failure: callback execution panics on insufficient gas provided by relayer",
|
|
func() {
|
|
packetData.Memo = fmt.Sprintf(`{"src_callback": {"address":"%s", "gas_limit":"%d"}}`, simapp.OogPanicContract, userGasLimit)
|
|
|
|
ctx = ctx.WithGasMeter(storetypes.NewGasMeter(300_000))
|
|
},
|
|
callbackFailed,
|
|
panicError,
|
|
},
|
|
{
|
|
"failure: callback execution fails",
|
|
func() {
|
|
packetData.Memo = fmt.Sprintf(`{"src_callback": {"address":"%s"}}`, simapp.ErrorContract)
|
|
},
|
|
callbackFailed,
|
|
nil, // execution failure in OnAcknowledgement should not block acknowledgement processing
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
s.Run(tc.name, func() {
|
|
s.SetupTest()
|
|
|
|
userGasLimit = 600000
|
|
packetData = transfertypes.NewFungibleTokenPacketData(
|
|
ibctesting.TestCoin.Denom,
|
|
ibctesting.TestCoin.Amount.String(),
|
|
ibctesting.TestAccAddress,
|
|
ibctesting.TestAccAddress,
|
|
fmt.Sprintf(`{"src_callback": {"address":"%s", "gas_limit":"%d"}}`, simapp.SuccessContract, userGasLimit),
|
|
)
|
|
|
|
ack = channeltypes.NewResultAcknowledgement([]byte{1}).Acknowledgement()
|
|
ctx = s.chainA.GetContext()
|
|
|
|
// may malleate packetData, ack, and ctx
|
|
tc.malleate()
|
|
|
|
payload := channeltypesv2.NewPayload(
|
|
transfertypes.PortID, transfertypes.PortID,
|
|
transfertypes.V1, transfertypes.EncodingJSON,
|
|
packetData.GetBytes(),
|
|
)
|
|
|
|
gasLimit := ctx.GasMeter().Limit()
|
|
|
|
// callbacks module is routed as top level middleware
|
|
cbs := s.chainA.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort)
|
|
|
|
onAcknowledgementPacket := func() error {
|
|
return cbs.OnAcknowledgementPacket(ctx, s.path.EndpointA.ClientID, s.path.EndpointB.ClientID, 1, ack, payload, s.chainA.SenderAccount.GetAddress())
|
|
}
|
|
|
|
switch tc.expError {
|
|
case nil:
|
|
err := onAcknowledgementPacket()
|
|
s.Require().Nil(err)
|
|
|
|
case panicError:
|
|
s.Require().PanicsWithValue(storetypes.ErrorOutOfGas{
|
|
Descriptor: fmt.Sprintf("ibc %s callback out of gas; commitGasLimit: %d", types.CallbackTypeAcknowledgementPacket, userGasLimit),
|
|
}, func() {
|
|
_ = onAcknowledgementPacket()
|
|
})
|
|
|
|
default:
|
|
err := onAcknowledgementPacket()
|
|
s.Require().ErrorIs(err, tc.expError)
|
|
}
|
|
|
|
sourceStatefulCounter := GetSimApp(s.chainA).MockContractKeeper.GetStateEntryCounter(s.chainA.GetContext())
|
|
sourceCounters := GetSimApp(s.chainA).MockContractKeeper.Counters
|
|
|
|
switch tc.expResult {
|
|
case noExecution:
|
|
s.Require().Len(sourceCounters, 0)
|
|
s.Require().Equal(uint8(0), sourceStatefulCounter)
|
|
|
|
case callbackFailed:
|
|
s.Require().Len(sourceCounters, 1)
|
|
s.Require().Equal(1, sourceCounters[types.CallbackTypeAcknowledgementPacket])
|
|
s.Require().Equal(uint8(0), sourceStatefulCounter)
|
|
|
|
case callbackSuccess:
|
|
s.Require().Len(sourceCounters, 1)
|
|
s.Require().Equal(1, sourceCounters[types.CallbackTypeAcknowledgementPacket])
|
|
s.Require().Equal(uint8(1), sourceStatefulCounter)
|
|
|
|
expEvent, exists := GetExpectedEvent(
|
|
ctx, packetData, gasLimit, payload.Version,
|
|
payload.SourcePort, s.path.EndpointA.ClientID, 1, types.CallbackTypeAcknowledgementPacket, nil,
|
|
)
|
|
s.Require().True(exists)
|
|
s.Require().Contains(ctx.EventManager().Events().ToABCIEvents(), expEvent)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *CallbacksTestSuite) TestOnTimeoutPacket() {
|
|
type expResult uint8
|
|
const (
|
|
noExecution expResult = iota
|
|
callbackFailed
|
|
callbackSuccess
|
|
)
|
|
|
|
var (
|
|
packetData transfertypes.FungibleTokenPacketData
|
|
ctx sdk.Context
|
|
)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
malleate func()
|
|
expResult expResult
|
|
expValue any
|
|
}{
|
|
{
|
|
"success",
|
|
func() {},
|
|
callbackSuccess,
|
|
nil,
|
|
},
|
|
{
|
|
"success: callback data nonexistent",
|
|
func() {
|
|
//nolint:goconst
|
|
packetData.Memo = ""
|
|
},
|
|
noExecution,
|
|
nil,
|
|
},
|
|
{
|
|
"failure: underlying app OnTimeoutPacket fails",
|
|
func() {
|
|
packetData.Amount = "invalid amount"
|
|
},
|
|
noExecution,
|
|
transfertypes.ErrInvalidAmount,
|
|
},
|
|
{
|
|
"failure: callback data is not valid",
|
|
func() {
|
|
//nolint:goconst
|
|
packetData.Memo = `{"src_callback": {"address": ""}}`
|
|
},
|
|
noExecution,
|
|
types.ErrInvalidCallbackData,
|
|
},
|
|
{
|
|
"failure: callback execution reach out of gas, but sufficient gas provided by relayer",
|
|
func() {
|
|
packetData.Memo = fmt.Sprintf(`{"src_callback": {"address":"%s", "gas_limit":"400000"}}`, simapp.OogPanicContract)
|
|
},
|
|
callbackFailed,
|
|
nil,
|
|
},
|
|
{
|
|
"failure: callback execution panics on insufficient gas provided by relayer",
|
|
func() {
|
|
packetData.Memo = fmt.Sprintf(`{"src_callback": {"address":"%s"}}`, simapp.OogPanicContract)
|
|
|
|
ctx = ctx.WithGasMeter(storetypes.NewGasMeter(300_000))
|
|
},
|
|
callbackFailed,
|
|
storetypes.ErrorOutOfGas{
|
|
Descriptor: fmt.Sprintf("ibc %s callback out of gas; commitGasLimit: %d", types.CallbackTypeTimeoutPacket, maxCallbackGas),
|
|
},
|
|
},
|
|
{
|
|
"failure: callback execution fails",
|
|
func() {
|
|
packetData.Memo = fmt.Sprintf(`{"src_callback": {"address":"%s"}}`, simapp.ErrorContract)
|
|
},
|
|
callbackFailed,
|
|
nil, // execution failure in OnTimeout should not block timeout processing
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
s.Run(tc.name, func() {
|
|
s.SetupTest()
|
|
|
|
// NOTE: we call send packet so transfer is setup with the correct logic to
|
|
// succeed on timeout
|
|
userGasLimit := 600_000
|
|
timeoutTimestamp := uint64(s.chainB.GetContext().BlockTime().Add(time.Second).Unix())
|
|
packetData = transfertypes.NewFungibleTokenPacketData(
|
|
ibctesting.TestCoin.Denom,
|
|
ibctesting.TestCoin.Amount.String(),
|
|
s.chainA.SenderAccount.GetAddress().String(),
|
|
ibctesting.TestAccAddress,
|
|
fmt.Sprintf(`{"src_callback": {"address":"%s", "gas_limit":"%d"}}`, simapp.SuccessContract, userGasLimit),
|
|
)
|
|
|
|
payload := channeltypesv2.NewPayload(
|
|
transfertypes.PortID, transfertypes.PortID,
|
|
transfertypes.V1, transfertypes.EncodingJSON,
|
|
packetData.GetBytes(),
|
|
)
|
|
|
|
packet, err := s.path.EndpointA.MsgSendPacket(timeoutTimestamp, payload)
|
|
s.Require().NoError(err)
|
|
|
|
ctx = s.chainA.GetContext()
|
|
gasLimit := ctx.GasMeter().Limit()
|
|
|
|
tc.malleate()
|
|
|
|
// update packet data in payload after malleate
|
|
payload.Value = packetData.GetBytes()
|
|
|
|
// callbacks module is routed as top level middleware
|
|
cbs := s.chainA.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort)
|
|
|
|
onTimeoutPacket := func() error {
|
|
return cbs.OnTimeoutPacket(ctx, s.path.EndpointA.ClientID, s.path.EndpointB.ClientID, 1, payload, s.chainA.SenderAccount.GetAddress())
|
|
}
|
|
|
|
switch expValue := tc.expValue.(type) {
|
|
case nil:
|
|
err := onTimeoutPacket()
|
|
s.Require().Nil(err)
|
|
case error:
|
|
err := onTimeoutPacket()
|
|
s.Require().ErrorIs(err, expValue)
|
|
default:
|
|
s.Require().PanicsWithValue(tc.expValue, func() {
|
|
_ = onTimeoutPacket()
|
|
})
|
|
}
|
|
|
|
sourceStatefulCounter := GetSimApp(s.chainA).MockContractKeeper.GetStateEntryCounter(s.chainA.GetContext())
|
|
sourceCounters := GetSimApp(s.chainA).MockContractKeeper.Counters
|
|
|
|
// account for SendPacket succeeding
|
|
switch tc.expResult {
|
|
case noExecution:
|
|
s.Require().Len(sourceCounters, 1)
|
|
s.Require().Equal(uint8(1), sourceStatefulCounter)
|
|
|
|
case callbackFailed:
|
|
s.Require().Len(sourceCounters, 2)
|
|
s.Require().Equal(1, sourceCounters[types.CallbackTypeTimeoutPacket])
|
|
s.Require().Equal(1, sourceCounters[types.CallbackTypeSendPacket])
|
|
s.Require().Equal(uint8(1), sourceStatefulCounter)
|
|
|
|
case callbackSuccess:
|
|
s.Require().Len(sourceCounters, 2)
|
|
s.Require().Equal(1, sourceCounters[types.CallbackTypeTimeoutPacket])
|
|
s.Require().Equal(1, sourceCounters[types.CallbackTypeSendPacket])
|
|
s.Require().Equal(uint8(2), sourceStatefulCounter)
|
|
|
|
expEvent, exists := GetExpectedEvent(
|
|
ctx, packetData, gasLimit, payload.Version,
|
|
payload.SourcePort, s.path.EndpointA.ClientID, packet.Sequence, types.CallbackTypeTimeoutPacket, nil,
|
|
)
|
|
s.Require().True(exists)
|
|
s.Require().Contains(ctx.EventManager().Events().ToABCIEvents(), expEvent)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *CallbacksTestSuite) TestOnRecvPacket() {
|
|
type expResult uint8
|
|
type expRecvStatus uint8
|
|
const (
|
|
noExecution expResult = iota
|
|
callbackFailed
|
|
callbackPanic
|
|
callbackSuccess
|
|
)
|
|
const (
|
|
success expRecvStatus = iota
|
|
panics
|
|
failure
|
|
)
|
|
|
|
var (
|
|
packetData transfertypes.FungibleTokenPacketData
|
|
ctx sdk.Context
|
|
userGasLimit uint64
|
|
)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
malleate func()
|
|
expResult expResult
|
|
expRecvStatus expRecvStatus
|
|
}{
|
|
{
|
|
"success",
|
|
func() {},
|
|
callbackSuccess,
|
|
success,
|
|
},
|
|
{
|
|
"success: callback data nonexistent",
|
|
func() {
|
|
//nolint:goconst
|
|
packetData.Memo = ""
|
|
},
|
|
noExecution,
|
|
success,
|
|
},
|
|
{
|
|
"failure: underlying app OnRecvPacket fails",
|
|
func() {
|
|
packetData.Denom = ""
|
|
},
|
|
noExecution,
|
|
failure,
|
|
},
|
|
{
|
|
"failure: no-op on callback data is not valid",
|
|
func() {
|
|
//nolint:goconst
|
|
packetData.Memo = `{"dest_callback": {"address": ""}}`
|
|
},
|
|
noExecution,
|
|
failure,
|
|
},
|
|
{
|
|
"failure: callback execution reach out of gas, but sufficient gas provided by relayer",
|
|
func() {
|
|
packetData.Memo = fmt.Sprintf(`{"dest_callback": {"address":"%s", "gas_limit":"%d"}}`, simapp.OogPanicContract, userGasLimit)
|
|
},
|
|
callbackFailed,
|
|
failure,
|
|
},
|
|
{
|
|
"failure: callback execution panics on insufficient gas provided by relayer",
|
|
func() {
|
|
packetData.Memo = fmt.Sprintf(`{"dest_callback": {"address":"%s", "gas_limit":"%d"}}`, simapp.OogPanicContract, userGasLimit)
|
|
|
|
ctx = ctx.WithGasMeter(storetypes.NewGasMeter(300_000))
|
|
},
|
|
callbackFailed,
|
|
panics,
|
|
},
|
|
{
|
|
"failure: callback execution fails",
|
|
func() {
|
|
packetData.Memo = fmt.Sprintf(`{"dest_callback": {"address":"%s"}}`, simapp.ErrorContract)
|
|
},
|
|
callbackFailed,
|
|
failure,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
s.Run(tc.name, func() {
|
|
s.SetupTest()
|
|
|
|
// set user gas limit above panic level in mock contract keeper
|
|
userGasLimit = 600_000
|
|
packetData = transfertypes.NewFungibleTokenPacketData(
|
|
ibctesting.TestCoin.Denom,
|
|
ibctesting.TestCoin.Amount.String(),
|
|
ibctesting.TestAccAddress,
|
|
s.chainB.SenderAccount.GetAddress().String(),
|
|
fmt.Sprintf(`{"dest_callback": {"address":"%s", "gas_limit":"%d"}}`, ibctesting.TestAccAddress, userGasLimit),
|
|
)
|
|
|
|
payload := channeltypesv2.NewPayload(
|
|
transfertypes.PortID, transfertypes.PortID,
|
|
transfertypes.V1, transfertypes.EncodingJSON,
|
|
packetData.GetBytes(),
|
|
)
|
|
|
|
ctx = s.chainB.GetContext()
|
|
gasLimit := ctx.GasMeter().Limit()
|
|
|
|
tc.malleate()
|
|
|
|
// update packet data in payload after malleate
|
|
payload.Value = packetData.GetBytes()
|
|
|
|
// callbacks module is routed as top level middleware
|
|
cbs := s.chainB.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort)
|
|
|
|
onRecvPacket := func() channeltypesv2.RecvPacketResult {
|
|
return cbs.OnRecvPacket(ctx, s.path.EndpointA.ClientID, s.path.EndpointB.ClientID, 1, payload, s.chainB.SenderAccount.GetAddress())
|
|
}
|
|
|
|
switch tc.expRecvStatus {
|
|
case success:
|
|
recvResult := onRecvPacket()
|
|
s.Require().Equal(channeltypesv2.PacketStatus_Success, recvResult.Status)
|
|
|
|
case panics:
|
|
s.Require().PanicsWithValue(storetypes.ErrorOutOfGas{
|
|
Descriptor: fmt.Sprintf("ibc %s callback out of gas; commitGasLimit: %d", types.CallbackTypeReceivePacket, userGasLimit),
|
|
}, func() {
|
|
_ = onRecvPacket()
|
|
})
|
|
|
|
default:
|
|
recvResult := onRecvPacket()
|
|
s.Require().Equal(channeltypesv2.PacketStatus_Failure, recvResult.Status)
|
|
}
|
|
|
|
destStatefulCounter := GetSimApp(s.chainB).MockContractKeeper.GetStateEntryCounter(s.chainB.GetContext())
|
|
destCounters := GetSimApp(s.chainB).MockContractKeeper.Counters
|
|
|
|
switch tc.expResult {
|
|
case noExecution:
|
|
s.Require().Len(destCounters, 0)
|
|
s.Require().Equal(uint8(0), destStatefulCounter)
|
|
|
|
case callbackFailed:
|
|
s.Require().Len(destCounters, 1)
|
|
s.Require().Equal(1, destCounters[types.CallbackTypeReceivePacket])
|
|
s.Require().Equal(uint8(0), destStatefulCounter)
|
|
|
|
case callbackSuccess:
|
|
s.Require().Len(destCounters, 1)
|
|
s.Require().Equal(1, destCounters[types.CallbackTypeReceivePacket])
|
|
s.Require().Equal(uint8(1), destStatefulCounter)
|
|
|
|
expEvent, exists := GetExpectedEvent(
|
|
ctx, packetData, gasLimit, payload.Version,
|
|
payload.DestinationPort, s.path.EndpointB.ClientID, 1, types.CallbackTypeReceivePacket, nil,
|
|
)
|
|
s.Require().True(exists)
|
|
s.Require().Contains(ctx.EventManager().Events().ToABCIEvents(), expEvent)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *CallbacksTestSuite) TestWriteAcknowledgement() {
|
|
var (
|
|
packetData transfertypes.FungibleTokenPacketData
|
|
destClient string
|
|
ctx sdk.Context
|
|
ack channeltypesv2.Acknowledgement
|
|
multiPayload bool
|
|
)
|
|
|
|
successAck := channeltypesv2.NewAcknowledgement(channeltypes.NewResultAcknowledgement([]byte{byte(1)}).Acknowledgement())
|
|
|
|
testCases := []struct {
|
|
name string
|
|
malleate func()
|
|
callbackType types.CallbackType
|
|
expError error
|
|
}{
|
|
{
|
|
"success",
|
|
func() {
|
|
ack = successAck
|
|
},
|
|
types.CallbackTypeReceivePacket,
|
|
nil,
|
|
},
|
|
{
|
|
"success: callback data nonexistent",
|
|
func() {
|
|
packetData.Memo = ""
|
|
ack = successAck
|
|
},
|
|
"none",
|
|
nil,
|
|
},
|
|
{
|
|
"failure: callback data is not valid",
|
|
func() {
|
|
packetData.Memo = `{"dest_callback": {"address": ""}}`
|
|
},
|
|
"none", // improperly formatted callback data should result in no callback execution
|
|
types.ErrInvalidCallbackData,
|
|
},
|
|
{
|
|
"failure: ics4Wrapper WriteAcknowledgement call fails",
|
|
func() {
|
|
destClient = "invalid-client"
|
|
},
|
|
"none",
|
|
channeltypesv2.ErrInvalidAcknowledgement,
|
|
},
|
|
{
|
|
"failure: multipayload should fail",
|
|
func() {
|
|
multiPayload = true
|
|
},
|
|
"none",
|
|
channeltypesv2.ErrInvalidAcknowledgement,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
s.Run(tc.name, func() {
|
|
s.SetupTest()
|
|
|
|
// set user gas limit above panic level in mock contract keeper
|
|
packetData = transfertypes.NewFungibleTokenPacketData(
|
|
ibctesting.TestCoin.Denom,
|
|
ibctesting.TestCoin.Amount.String(),
|
|
ibctesting.TestAccAddress,
|
|
s.chainB.SenderAccount.GetAddress().String(),
|
|
fmt.Sprintf(`{"dest_callback": {"address":"%s", "gas_limit":"600000"}}`, ibctesting.TestAccAddress),
|
|
)
|
|
|
|
ctx = s.chainB.GetContext()
|
|
gasLimit := ctx.GasMeter().Limit()
|
|
destClient = s.path.EndpointB.ClientID
|
|
|
|
tc.malleate()
|
|
|
|
payload := channeltypesv2.NewPayload(
|
|
transfertypes.PortID, transfertypes.PortID,
|
|
transfertypes.V1, transfertypes.EncodingJSON,
|
|
packetData.GetBytes(),
|
|
)
|
|
timeoutTimestamp := uint64(s.chainB.GetContext().BlockTime().Unix())
|
|
var packet channeltypesv2.Packet
|
|
if multiPayload {
|
|
packet = channeltypesv2.NewPacket(
|
|
1, s.path.EndpointA.ClientID, s.path.EndpointB.ClientID,
|
|
timeoutTimestamp, payload, payload,
|
|
)
|
|
} else {
|
|
packet = channeltypesv2.NewPacket(
|
|
1, s.path.EndpointA.ClientID, s.path.EndpointB.ClientID,
|
|
timeoutTimestamp, payload,
|
|
)
|
|
}
|
|
// mock async receive manually so WriteAcknowledgement can pass
|
|
s.chainB.App.GetIBCKeeper().ChannelKeeperV2.SetAsyncPacket(ctx, packet.DestinationClient, packet.Sequence, packet)
|
|
s.chainB.App.GetIBCKeeper().ChannelKeeperV2.SetPacketReceipt(ctx, packet.DestinationClient, packet.Sequence)
|
|
|
|
// callbacks module is routed as top level middleware
|
|
cbs := s.chainB.App.GetIBCKeeper().ChannelKeeperV2.Router.Route(ibctesting.TransferPort)
|
|
mw, ok := cbs.(api.WriteAcknowledgementWrapper)
|
|
s.Require().True(ok)
|
|
|
|
err := mw.WriteAcknowledgement(ctx, destClient, packet.Sequence, ack)
|
|
|
|
expPass := tc.expError == nil
|
|
s.AssertHasExecutedExpectedCallback(tc.callbackType, expPass)
|
|
|
|
if expPass {
|
|
s.Require().NoError(err)
|
|
|
|
expEvent, exists := GetExpectedEvent(
|
|
ctx, packetData, gasLimit, payload.Version,
|
|
payload.DestinationPort, packet.DestinationClient, packet.Sequence, types.CallbackTypeReceivePacket, nil,
|
|
)
|
|
if exists {
|
|
s.Require().Contains(ctx.EventManager().Events().ToABCIEvents(), expEvent)
|
|
}
|
|
|
|
} else {
|
|
s.Require().ErrorIs(err, tc.expError)
|
|
}
|
|
})
|
|
}
|
|
}
|