Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(test) Add tests for callback execution when forwarding a packet #6805

Merged
merged 5 commits into from
Jul 18, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions modules/apps/callbacks/forwarding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package ibccallbacks_test
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had to add a new file here in the callbacks module because it's the only module in which we can check the callbacks counter. This required setting up a new, small test suite that creates 3 chains.


import (
"fmt"
"testing"

"github.com/stretchr/testify/suite"

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

"github.com/cosmos/ibc-go/modules/apps/callbacks/testing/simapp"
callbacktypes "github.com/cosmos/ibc-go/modules/apps/callbacks/types"
"github.com/cosmos/ibc-go/v8/modules/apps/transfer/types"
clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types"
channeltypes "github.com/cosmos/ibc-go/v8/modules/core/04-channel/types"
ibctesting "github.com/cosmos/ibc-go/v8/testing"
)

func init() {
ibctesting.DefaultTestingAppInit = SetupTestingApp
}

type CallbacksForwardingTestSuite struct {
suite.Suite

coordinator *ibctesting.Coordinator

chainA *ibctesting.TestChain
chainB *ibctesting.TestChain
chainC *ibctesting.TestChain

pathAtoB *ibctesting.Path
pathBtoC *ibctesting.Path
}

// setupChains sets up a coordinator with 3 test chains.
func (s *CallbacksForwardingTestSuite) setupChains() {
s.coordinator = ibctesting.NewCoordinator(s.T(), 3)
s.chainA = s.coordinator.GetChain(ibctesting.GetChainID(1))
s.chainB = s.coordinator.GetChain(ibctesting.GetChainID(2))
s.chainC = s.coordinator.GetChain(ibctesting.GetChainID(3))

s.pathAtoB = ibctesting.NewTransferPath(s.chainA, s.chainB)
s.pathBtoC = ibctesting.NewTransferPath(s.chainB, s.chainC)
}

func (s *CallbacksForwardingTestSuite) SetupTest() {
s.setupChains()

s.pathAtoB.Setup()
s.pathBtoC.Setup()
}

func TestIBCCallbacksForwardingTestsuite(t *testing.T) {
suite.Run(t, new(CallbacksForwardingTestSuite))
}

// TestForwardingWithMemoCallback tests that, when forwarding a packet with memo from A to B to C,
// the callback is executed only on the final hop.
// NOTE: this does not test the full forwarding behaviour (assert on amounts, packets, acks etc)
// as this is covered in other forwarding tests.
func (s *CallbacksForwardingTestSuite) TestForwardingWithMemoCallback() {
testCases := []struct {
name string
testMemo string
expCallbackMapOnChainB map[callbacktypes.CallbackType]int
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We discussed this with @chatton and our understanding is that when the memo specifies src_callback we expect the callback to be executed on B and not on C. basically when forwarding we ensure that all callbacks are executed on either the last hop (C) or the one immediately before that (B).

If that's not the case, please let me know! I was surprised to see some callbacks executed on B but after some reasoning it started to make sense.

Copy link
Member

Choose a reason for hiding this comment

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

With the current implementation it makes sense that this would be the case.. but it is a little weird imo.

Wonder if we should have a testcase for ensuring that the send_packet callback is never invoked on chainA

Copy link
Contributor

Choose a reason for hiding this comment

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

yea very strange, but I guess not much we can do. Would be great to add into the docs somewhere? With multi packetdata, it should become more explicit when and where callbacks are executed (hopefully 😅)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I opened #6883 to ensure we document this properly

expCallbackMapOnChainC map[callbacktypes.CallbackType]int
}{
{
name: "no memo",
expCallbackMapOnChainB: map[callbacktypes.CallbackType]int{},
expCallbackMapOnChainC: map[callbacktypes.CallbackType]int{},
},
{
name: "recv callback",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's still not super clear to me what's the mapping between Memo content and which callbacks are executed. Does anyone have a pointer to learn this better?

Copy link
Contributor

Choose a reason for hiding this comment

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

the main thing is that the special keys in the callback data specify on which chain the callback should be executed.

Which is why that in this test case, it is only being executed on the dest chain. and not the src (which would be chain B in this case)

testMemo: fmt.Sprintf(`{"dest_callback": {"address": "%s"}}`, simapp.SuccessContract),
expCallbackMapOnChainB: map[callbacktypes.CallbackType]int{},
expCallbackMapOnChainC: map[callbacktypes.CallbackType]int{callbacktypes.CallbackTypeReceivePacket: 1},
},
{
name: "ack callback",
testMemo: fmt.Sprintf(`{"src_callback": {"address": "%s"}}`, simapp.SuccessContract),
expCallbackMapOnChainB: map[callbacktypes.CallbackType]int{callbacktypes.CallbackTypeAcknowledgementPacket: 1},
expCallbackMapOnChainC: map[callbacktypes.CallbackType]int{},
},
Comment on lines +80 to +85
Copy link
Contributor

@chatton chatton Jul 15, 2024

Choose a reason for hiding this comment

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

might be worth adding a memo that contains both src_callback and dest_callback, in the happy path I believe we should have

expCallbackMapOnChainB: map[callbacktypes.CallbackType]int{callbacktypes.CallbackTypeAcknowledgementPacket: 1}

expCallbackMapOnChainC: map[callbacktypes.CallbackType]int{callbacktypes.CallbackTypeReceivePacket: 1},

Copy link
Contributor

Choose a reason for hiding this comment

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

		{
			name:                   "ack and recv callback",
			testMemo:               fmt.Sprintf(`{"src_callback": {"address": "%s"}, "dest_callback": {"address": "%s"}}`, simapp.SuccessContract, simapp.SuccessContract),
			expCallbackMapOnChainB: map[callbacktypes.CallbackType]int{callbacktypes.CallbackTypeAcknowledgementPacket: 1},
			expCallbackMapOnChainC: map[callbacktypes.CallbackType]int{callbacktypes.CallbackTypeReceivePacket: 1},
		},

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good one thank you! added!

{
name: "ack and recv callback",
testMemo: fmt.Sprintf(`{"src_callback": {"address": "%s"}, "dest_callback": {"address": "%s"}}`, simapp.SuccessContract, simapp.SuccessContract),
expCallbackMapOnChainB: map[callbacktypes.CallbackType]int{callbacktypes.CallbackTypeAcknowledgementPacket: 1},
expCallbackMapOnChainC: map[callbacktypes.CallbackType]int{callbacktypes.CallbackTypeReceivePacket: 1},
},
{
name: "ack callback with low gas (error)",
testMemo: fmt.Sprintf(`{"src_callback": {"address": "%s"}}`, simapp.OogErrorContract),
expCallbackMapOnChainB: map[callbacktypes.CallbackType]int{callbacktypes.CallbackTypeAcknowledgementPacket: 1},
expCallbackMapOnChainC: map[callbacktypes.CallbackType]int{},
},
{
name: "recv callback with low gas (error)",
testMemo: fmt.Sprintf(`{"dest_callback": {"address": "%s"}}`, simapp.OogErrorContract),
expCallbackMapOnChainB: map[callbacktypes.CallbackType]int{},
expCallbackMapOnChainC: map[callbacktypes.CallbackType]int{callbacktypes.CallbackTypeReceivePacket: 1},
},
}
for _, tc := range testCases {
s.Run(tc.name, func() {
s.SetupTest()

coinOnA := ibctesting.TestCoin
sender := s.chainA.SenderAccounts[0].SenderAccount
receiver := s.chainC.SenderAccounts[0].SenderAccount
forwarding := types.NewForwarding(false, types.NewHop(
s.pathBtoC.EndpointA.ChannelConfig.PortID,
s.pathBtoC.EndpointA.ChannelID,
))
successAck := channeltypes.NewResultAcknowledgement([]byte{byte(1)})

transferMsg := types.NewMsgTransfer(
s.pathAtoB.EndpointA.ChannelConfig.PortID,
s.pathAtoB.EndpointA.ChannelID,
sdk.NewCoins(coinOnA),
sender.GetAddress().String(),
receiver.GetAddress().String(),
clienttypes.ZeroHeight(),
s.chainA.GetTimeoutTimestamp(),
tc.testMemo,
forwarding,
)

result, err := s.chainA.SendMsgs(transferMsg)
s.Require().NoError(err) // message committed

// parse the packet from result events and recv packet on chainB
packetFromAtoB, err := ibctesting.ParsePacketFromEvents(result.Events)
s.Require().NoError(err)
s.Require().NotNil(packetFromAtoB)

err = s.pathAtoB.EndpointB.UpdateClient()
s.Require().NoError(err)

result, err = s.pathAtoB.EndpointB.RecvPacketWithResult(packetFromAtoB)
s.Require().NoError(err)
s.Require().NotNil(result)

packetFromBtoC, err := ibctesting.ParsePacketFromEvents(result.Events)
s.Require().NoError(err)
s.Require().NotNil(packetFromBtoC)

err = s.pathBtoC.EndpointA.UpdateClient()
s.Require().NoError(err)

err = s.pathBtoC.EndpointB.UpdateClient()
s.Require().NoError(err)

result, err = s.pathBtoC.EndpointB.RecvPacketWithResult(packetFromBtoC)
s.Require().NoError(err)
s.Require().NotNil(result)

packetOnC, err := ibctesting.ParseRecvPacketFromEvents(result.Events)
s.Require().NoError(err)
s.Require().NotNil(packetOnC)

// Ack back to B
err = s.pathBtoC.EndpointB.UpdateClient()
s.Require().NoError(err)

err = s.pathBtoC.EndpointA.AcknowledgePacket(packetFromBtoC, successAck.Acknowledgement())
s.Require().NoError(err)

// Ack back to A
err = s.pathAtoB.EndpointA.UpdateClient()
s.Require().NoError(err)

err = s.pathAtoB.EndpointA.AcknowledgePacket(packetFromAtoB, successAck.Acknowledgement())
s.Require().NoError(err)

// We never expect chainA to have executed any callback
s.Require().Empty(GetSimApp(s.chainA).MockContractKeeper.Counters, "chainA's callbacks counter map is not empty")
Copy link
Member

Choose a reason for hiding this comment

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

maybe this covers my thoughts on any source chain callback being invoked on A!


// We expect chainB to have executed callbacks when the memo is of type `src_callback`
chainBCallbackMap := GetSimApp(s.chainB).MockContractKeeper.Counters
s.Require().Equal(tc.expCallbackMapOnChainB, chainBCallbackMap, "chainB: expected callback counters map to be %v, got %v instead", tc.expCallbackMapOnChainB, chainBCallbackMap)

// We expect chainC to have executed callbacks when the memo is of type `dest_callback`
chainCCallbackMap := GetSimApp(s.chainC).MockContractKeeper.Counters
s.Require().Equal(tc.expCallbackMapOnChainC, chainCCallbackMap, "chainC: expected callback counters map to be %v, got %v instead", tc.expCallbackMapOnChainC, chainCCallbackMap)
})
}
}
Loading