From 0ccfe8947a5d7252660fc2f72ec8311d822eec7f Mon Sep 17 00:00:00 2001 From: Andrew Gouin Date: Sun, 25 Sep 2022 02:21:37 -0600 Subject: [PATCH] Add packet forward middleware with retry and refund --- chain/cosmos/chain_node.go | 16 ++- chain/cosmos/poll.go | 22 ++++ examples/ibc/packet_forward_test.go | 120 +++++++++++++------ examples/packet_forward_refund_test.go | 160 +++++++++++++++++++++++++ 4 files changed, 282 insertions(+), 36 deletions(-) create mode 100644 examples/packet_forward_refund_test.go diff --git a/chain/cosmos/chain_node.go b/chain/cosmos/chain_node.go index fd4c3d89e..4c1ab855e 100644 --- a/chain/cosmos/chain_node.go +++ b/chain/cosmos/chain_node.go @@ -226,7 +226,7 @@ func (tn *ChainNode) SetTestConfig(ctx context.Context) error { c["rpc"] = rpc - return configutil.ModifyTomlConfigFile( + if err := configutil.ModifyTomlConfigFile( ctx, tn.logger(), tn.DockerClient, @@ -234,6 +234,20 @@ func (tn *ChainNode) SetTestConfig(ctx context.Context) error { tn.VolumeName, "config/config.toml", c, + ); err != nil { + return err + } + + a := make(configutil.Toml) + a["minimum-gas-prices"] = tn.Chain.Config().GasPrices + return configutil.ModifyTomlConfigFile( + ctx, + tn.logger(), + tn.DockerClient, + tn.TestName, + tn.VolumeName, + "config/app.toml", + a, ) } diff --git a/chain/cosmos/poll.go b/chain/cosmos/poll.go index f4f5246ac..288ddd635 100644 --- a/chain/cosmos/poll.go +++ b/chain/cosmos/poll.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/strangelove-ventures/ibctest/v5/ibc" "github.com/strangelove-ventures/ibctest/v5/test" ) @@ -27,3 +28,24 @@ func PollForProposalStatus(ctx context.Context, chain *CosmosChain, startHeight, } return p.(ProposalResponse), nil } + +// PollForBalance polls until the balance matches +func PollForBalance(ctx context.Context, chain *CosmosChain, deltaBlocks uint64, balance ibc.WalletAmount) error { + h, err := chain.Height(ctx) + if err != nil { + return fmt.Errorf("failed to get height: %w", err) + } + doPoll := func(ctx context.Context, height uint64) (any, error) { + bal, err := chain.GetBalance(ctx, balance.Address, balance.Denom) + if err != nil { + return nil, err + } + if bal != balance.Amount { + return nil, fmt.Errorf("balance (%d) does not match expected: (%d)", bal, balance.Amount) + } + return nil, nil + } + bp := test.BlockPoller{CurrentHeight: chain.Height, PollFunc: doPoll} + _, err = bp.DoPoll(ctx, h, h+deltaBlocks) + return err +} diff --git a/examples/ibc/packet_forward_test.go b/examples/ibc/packet_forward_test.go index d8ff498e0..22028c57f 100644 --- a/examples/ibc/packet_forward_test.go +++ b/examples/ibc/packet_forward_test.go @@ -7,8 +7,8 @@ import ( transfertypes "github.com/cosmos/ibc-go/v5/modules/apps/transfer/types" "github.com/strangelove-ventures/ibctest/v5" + "github.com/strangelove-ventures/ibctest/v5/chain/cosmos" "github.com/strangelove-ventures/ibctest/v5/ibc" - "github.com/strangelove-ventures/ibctest/v5/test" "github.com/strangelove-ventures/ibctest/v5/testreporter" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" @@ -27,15 +27,21 @@ func TestPacketForwardMiddleware(t *testing.T) { ctx := context.Background() cf := ibctest.NewBuiltinChainFactory(zaptest.NewLogger(t), []*ibctest.ChainSpec{ - {Name: "gaia", ChainName: "gaia-fork", Version: "bugfix-replace_default_transfer_with_router_module"}, - {Name: "osmosis", ChainName: "osmosis", Version: "v11.0.1"}, - {Name: "juno", ChainName: "juno", Version: "v9.0.0"}, + {Name: "gaia", ChainConfig: ibc.ChainConfig{ + Images: []ibc.DockerImage{{ + Repository: "gaia", + Version: "local", + UidGid: "1025:1025", + }}, + }}, + {Name: "osmosis", Version: "v11.0.1"}, + {Name: "juno", Version: "v9.0.0"}, }) chains, err := cf.Chains(t.Name()) require.NoError(t, err) - gaia, osmosis, juno := chains[0], chains[1], chains[2] + gaia, osmosis, juno := chains[0].(*cosmos.CosmosChain), chains[1].(*cosmos.CosmosChain), chains[2].(*cosmos.CosmosChain) r := ibctest.NewBuiltinRelayerFactory( ibc.CosmosRly, @@ -102,9 +108,6 @@ func TestPacketForwardMiddleware(t *testing.T) { // Get original account balances osmosisUser, gaiaUser, junoUser := users[0], users[1], users[2] - osmosisBalOG, err := osmosis.GetBalance(ctx, osmosisUser.Bech32Address(osmosis.Config().Bech32Prefix), osmosis.Config().Denom) - require.NoError(t, err) - // Send packet from Osmosis->Hub->Juno // receiver format: {intermediate_refund_address}|{foward_port}/{forward_channel}:{final_destination_address} const transferAmount int64 = 100000 @@ -120,15 +123,6 @@ func TestPacketForwardMiddleware(t *testing.T) { _, err = osmosis.SendIBCTransfer(ctx, osmosisGaiaChan.ChannelID, osmosisUser.KeyName, transfer, nil) require.NoError(t, err) - // Wait for transfer to be relayed - err = test.WaitForBlocks(ctx, 10, gaia) - require.NoError(t, err) - - // Check that the funds sent are gone from the acc on osmosis - osmosisBal, err := osmosis.GetBalance(ctx, osmosisUser.Bech32Address(osmosis.Config().Bech32Prefix), osmosis.Config().Denom) - require.NoError(t, err) - require.Equal(t, osmosisBalOG-transferAmount, osmosisBal) - // Compose the prefixed denoms and ibc denom for asserting balances gaiaOsmoChan := osmoChannels[0].Counterparty junoGaiaChan := junoChannels[0] @@ -136,10 +130,21 @@ func TestPacketForwardMiddleware(t *testing.T) { secondHopDenom := transfertypes.GetPrefixedDenom(junoGaiaChan.Counterparty.PortID, junoGaiaChan.Counterparty.ChannelID, firstHopDenom) dstIbcDenom := transfertypes.ParseDenomTrace(secondHopDenom) + // Check that the funds sent are gone from the acc on osmosis + err = cosmos.PollForBalance(ctx, osmosis, 2, ibc.WalletAmount{ + Address: osmosisUser.Bech32Address(osmosis.Config().Bech32Prefix), + Denom: osmosis.Config().Denom, + Amount: userFunds - transferAmount, + }) + require.NoError(t, err) + // Check that the funds sent are present in the acc on juno - junoBal, err := juno.GetBalance(ctx, junoUser.Bech32Address(juno.Config().Bech32Prefix), dstIbcDenom.IBCDenom()) + err = cosmos.PollForBalance(ctx, juno, 15, ibc.WalletAmount{ + Address: junoUser.Bech32Address(juno.Config().Bech32Prefix), + Denom: dstIbcDenom.IBCDenom(), + Amount: transferAmount, + }) require.NoError(t, err) - require.Equal(t, transferAmount, junoBal) // Send packet back from Juno->Hub->Osmosis receiver = fmt.Sprintf("%s|%s/%s:%s", gaiaUser.Bech32Address(gaia.Config().Bech32Prefix), gaiaOsmoChan.PortID, gaiaOsmoChan.ChannelID, osmosisUser.Bech32Address(osmosis.Config().Bech32Prefix)) @@ -152,22 +157,24 @@ func TestPacketForwardMiddleware(t *testing.T) { _, err = juno.SendIBCTransfer(ctx, junoGaiaChan.ChannelID, junoUser.KeyName, transfer, nil) require.NoError(t, err) - // Wait for transfer to be relayed - err = test.WaitForBlocks(ctx, 10, gaia) - require.NoError(t, err) - // Check that the funds sent are gone from the acc on juno - junoBal, err = juno.GetBalance(ctx, junoUser.Bech32Address(juno.Config().Bech32Prefix), dstIbcDenom.IBCDenom()) + err = cosmos.PollForBalance(ctx, juno, 2, ibc.WalletAmount{ + Address: junoUser.Bech32Address(juno.Config().Bech32Prefix), + Denom: dstIbcDenom.IBCDenom(), + Amount: int64(0), + }) require.NoError(t, err) - require.Equal(t, int64(0), junoBal) // Check that the funds sent are present in the acc on osmosis - osmosisBal, err = osmosis.GetBalance(ctx, osmosisUser.Bech32Address(osmosis.Config().Bech32Prefix), osmosis.Config().Denom) + err = cosmos.PollForBalance(ctx, osmosis, 15, ibc.WalletAmount{ + Address: osmosisUser.Bech32Address(osmosis.Config().Bech32Prefix), + Denom: osmosis.Config().Denom, + Amount: userFunds, + }) require.NoError(t, err) - require.Equal(t, osmosisBalOG, osmosisBal) // Send a malformed packet with invalid receiver address from Osmosis->Hub->Juno - // This should succeed in the first hop and fail to make the second hop; funds should end up in the intermediary account. + // This should succeed in the first hop and fail to make the second hop; funds should then be refunded to osmosis. receiver = fmt.Sprintf("%s|%s/%s:%s", gaiaUser.Bech32Address(gaia.Config().Bech32Prefix), gaiaJunoChan.PortID, gaiaJunoChan.ChannelID, "xyz1t8eh66t2w5k67kwurmn5gqhtq6d2ja0vp7jmmq") transfer = ibc.WalletAmount{ Address: receiver, @@ -178,18 +185,61 @@ func TestPacketForwardMiddleware(t *testing.T) { _, err = osmosis.SendIBCTransfer(ctx, osmosisGaiaChan.ChannelID, osmosisUser.KeyName, transfer, nil) require.NoError(t, err) - // Wait for transfer to be relayed - err = test.WaitForBlocks(ctx, 10, gaia) + // Wait until the funds sent are gone from the acc on osmosis + err = cosmos.PollForBalance(ctx, osmosis, 2, ibc.WalletAmount{ + Address: osmosisUser.Bech32Address(osmosis.Config().Bech32Prefix), + Denom: osmosis.Config().Denom, + Amount: userFunds - transferAmount, + }) require.NoError(t, err) - // Check that the funds sent are gone from the acc on osmosis - osmosisBal, err = osmosis.GetBalance(ctx, osmosisUser.Bech32Address(osmosis.Config().Bech32Prefix), osmosis.Config().Denom) + // Wait until the funds sent are back in the acc on osmosis + err = cosmos.PollForBalance(ctx, osmosis, 15, ibc.WalletAmount{ + Address: osmosisUser.Bech32Address(osmosis.Config().Bech32Prefix), + Denom: osmosis.Config().Denom, + Amount: userFunds, + }) require.NoError(t, err) - require.Equal(t, osmosisBalOG-transferAmount, osmosisBal) - // Check that the funds sent ended up in the acc on gaia + // Check that the gaia account is empty intermediaryIBCDenom := transfertypes.ParseDenomTrace(firstHopDenom) gaiaBal, err := gaia.GetBalance(ctx, gaiaUser.Bech32Address(gaia.Config().Bech32Prefix), intermediaryIBCDenom.IBCDenom()) require.NoError(t, err) - require.Equal(t, transferAmount, gaiaBal) + require.Equal(t, int64(0), gaiaBal) + + // Send packet from Osmosis->Hub->Juno with the timeout so low that it can not make it from Hub to Juno, which should result in a refund from Hub to Osmosis after two retries. + // receiver format: {intermediate_refund_address}|{foward_port}/{forward_channel}:{final_destination_address}:{max_retries}:{timeout_duration} + receiver = fmt.Sprintf("%s|%s/%s:%s:%d:%s", gaiaUser.Bech32Address(gaia.Config().Bech32Prefix), gaiaJunoChan.PortID, gaiaJunoChan.ChannelID, junoUser.Bech32Address(juno.Config().Bech32Prefix), 2, "1s") + transfer = ibc.WalletAmount{ + Address: receiver, + Denom: osmosis.Config().Denom, + Amount: transferAmount, + } + + _, err = osmosis.SendIBCTransfer(ctx, osmosisGaiaChan.ChannelID, osmosisUser.KeyName, transfer, nil) + require.NoError(t, err) + + // Wait until the funds sent are gone from the acc on osmosis + err = cosmos.PollForBalance(ctx, osmosis, 2, ibc.WalletAmount{ + Address: osmosisUser.Bech32Address(osmosis.Config().Bech32Prefix), + Denom: osmosis.Config().Denom, + Amount: userFunds - transferAmount, + }) + require.NoError(t, err) + + // Wait until the funds leave the gaia wallet (attempting to send to juno) + err = cosmos.PollForBalance(ctx, gaia, 5, ibc.WalletAmount{ + Address: gaiaUser.Bech32Address(gaia.Config().Bech32Prefix), + Denom: intermediaryIBCDenom.IBCDenom(), + Amount: 0, + }) + require.NoError(t, err) + + // Wait until the funds are back in the acc on osmosis + err = cosmos.PollForBalance(ctx, osmosis, 15, ibc.WalletAmount{ + Address: osmosisUser.Bech32Address(osmosis.Config().Bech32Prefix), + Denom: osmosis.Config().Denom, + Amount: userFunds, + }) + require.NoError(t, err) } diff --git a/examples/packet_forward_refund_test.go b/examples/packet_forward_refund_test.go new file mode 100644 index 000000000..1d157b09d --- /dev/null +++ b/examples/packet_forward_refund_test.go @@ -0,0 +1,160 @@ +package ibctest_test + +import ( + "context" + "fmt" + "testing" + + transfertypes "github.com/cosmos/ibc-go/v5/modules/apps/transfer/types" + "github.com/strangelove-ventures/ibctest/v5" + "github.com/strangelove-ventures/ibctest/v5/ibc" + "github.com/strangelove-ventures/ibctest/v5/test" + "github.com/strangelove-ventures/ibctest/v5/testreporter" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +func TestPacketForwardMiddlewareRefund(t *testing.T) { + if testing.Short() { + t.Skip() + } + + client, network := ibctest.DockerSetup(t) + + rep := testreporter.NewNopReporter() + eRep := rep.RelayerExecReporter(t) + + ctx := context.Background() + + cf := ibctest.NewBuiltinChainFactory(zaptest.NewLogger(t), []*ibctest.ChainSpec{ + {Name: "gaia", Version: "andrew-packet_forward_middleware"}, + {Name: "osmosis", Version: "v11.0.1"}, + {Name: "juno", Version: "v9.0.0"}, + }) + + chains, err := cf.Chains(t.Name()) + require.NoError(t, err) + + gaia, osmosis, juno := chains[0], chains[1], chains[2] + + r := ibctest.NewBuiltinRelayerFactory( + ibc.CosmosRly, + zaptest.NewLogger(t), + ).Build( + t, client, network, + ) + + const pathOsmoHub = "osmohub" + const pathJunoHub = "junohub" + + ic := ibctest.NewInterchain(). + AddChain(osmosis). + AddChain(gaia). + AddChain(juno). + AddRelayer(r, "relayer"). + AddLink(ibctest.InterchainLink{ + Chain1: osmosis, + Chain2: gaia, + Relayer: r, + Path: pathOsmoHub, + }). + AddLink(ibctest.InterchainLink{ + Chain1: gaia, + Chain2: juno, + Relayer: r, + Path: pathJunoHub, + }) + + require.NoError(t, ic.Build(ctx, eRep, ibctest.InterchainBuildOptions{ + TestName: t.Name(), + Client: client, + NetworkID: network, + BlockDatabaseFile: ibctest.DefaultBlockDatabaseFilepath(), + + SkipPathCreation: false, + })) + t.Cleanup(func() { + _ = ic.Close() + }) + + const userFunds = int64(10_000_000_000) + users := ibctest.GetAndFundTestUsers(t, ctx, t.Name(), userFunds, osmosis, gaia, juno) + + osmoChannels, err := r.GetChannels(ctx, eRep, osmosis.Config().ChainID) + require.NoError(t, err) + + junoChannels, err := r.GetChannels(ctx, eRep, juno.Config().ChainID) + require.NoError(t, err) + + // Start the relayer on both paths + err = r.StartRelayer(ctx, eRep, pathOsmoHub, pathJunoHub) + require.NoError(t, err) + + t.Cleanup( + func() { + err := r.StopRelayer(ctx, eRep) + if err != nil { + t.Logf("an error occured while stopping the relayer: %s", err) + } + }, + ) + + // Get original account balances + osmosisUser, gaiaUser, junoUser := users[0], users[1], users[2] + + // Send packet from Osmosis->Hub->Juno + // receiver format: {intermediate_refund_address}|{foward_port}/{forward_channel}:{final_destination_address} + const transferAmount int64 = 100000 + gaiaJunoChan := junoChannels[0].Counterparty + + osmosisGaiaChan := osmoChannels[0] + + // Compose the prefixed denoms and ibc denom for asserting balances + gaiaOsmoChan := osmoChannels[0].Counterparty + junoGaiaChan := junoChannels[0] + firstHopDenom := transfertypes.GetPrefixedDenom(gaiaOsmoChan.PortID, gaiaOsmoChan.ChannelID, osmosis.Config().Denom) + secondHopDenom := transfertypes.GetPrefixedDenom(junoGaiaChan.Counterparty.PortID, junoGaiaChan.Counterparty.ChannelID, firstHopDenom) + intermediaryIBCDenom := transfertypes.ParseDenomTrace(firstHopDenom) + dstIbcDenom := transfertypes.ParseDenomTrace(secondHopDenom) + + osmosisBal, err := osmosis.GetBalance(ctx, osmosisUser.Bech32Address(osmosis.Config().Bech32Prefix), osmosis.Config().Denom) + require.NoError(t, err) + + gaiaBal, err := gaia.GetBalance(ctx, gaiaUser.Bech32Address(gaia.Config().Bech32Prefix), intermediaryIBCDenom.IBCDenom()) + require.NoError(t, err) + + junoBal, err := juno.GetBalance(ctx, junoUser.Bech32Address(juno.Config().Bech32Prefix), dstIbcDenom.IBCDenom()) + require.NoError(t, err) + + // Send packet from Osmosis->Hub->Juno with the timeout so low that it can not make it from Hub to Juno, which should result in a refund from Hub to Osmosis. + // receiver format: {intermediate_refund_address}|{foward_port}/{forward_channel}:{final_destination_address}:{max_retries}:{timeout_duration} + receiver := fmt.Sprintf("%s|%s/%s:%s:%d:%s", gaiaUser.Bech32Address(gaia.Config().Bech32Prefix), gaiaJunoChan.PortID, gaiaJunoChan.ChannelID, junoUser.Bech32Address(juno.Config().Bech32Prefix), 2, "1s") + transfer := ibc.WalletAmount{ + Address: receiver, + Denom: osmosis.Config().Denom, + Amount: transferAmount, + } + + _, err = osmosis.SendIBCTransfer(ctx, osmosisGaiaChan.ChannelID, osmosisUser.KeyName, transfer, nil) + require.NoError(t, err) + + // Wait for transfer to be relayed + err = test.WaitForBlocks(ctx, 50, gaia, juno, osmosis) + require.NoError(t, err) + + // Check that the balances are the same as before + + osmosisBalPostRefund, err := osmosis.GetBalance(ctx, osmosisUser.Bech32Address(osmosis.Config().Bech32Prefix), osmosis.Config().Denom) + require.NoError(t, err) + require.Equal(t, osmosisBal, osmosisBalPostRefund) + + gaiaBalPostRefund, err := gaia.GetBalance(ctx, gaiaUser.Bech32Address(gaia.Config().Bech32Prefix), intermediaryIBCDenom.IBCDenom()) + require.NoError(t, err) + require.Equal(t, gaiaBal, gaiaBalPostRefund) + + // Check that the funds sent are present in the acc on juno + junoBalPostRefund, err := juno.GetBalance(ctx, junoUser.Bech32Address(juno.Config().Bech32Prefix), dstIbcDenom.IBCDenom()) + require.NoError(t, err) + require.Equal(t, junoBal, junoBalPostRefund) + +}