From d8b456e35ef1d51a56e2f7800989a160ddfdff9c Mon Sep 17 00:00:00 2001 From: Andrew Gouin Date: Tue, 8 Nov 2022 15:58:43 -0700 Subject: [PATCH] packet-forward-middleware packet memo, retry on timeout, and atomic forwards (#306) * Make examples its own module * Make cosmos specific module and run in CI in separate task * Update e2e test for memo refactor * Update test to chain-level params * Use gaia with pfm with configurable timeout and retries * Update SendIBCTransfer uses * fix interchain test * Add GetClients method to Relayer and helper for getting transfer channel between chains * Update packet forward test for multi-hop, add multi-hop refund test * Update tests for atomic forwards * Wait for a block after ack before checking balances * reduce wait to 1 block * Add multi-hop flow with refund through chain with native denom. Add assertions for escrow accounts * Remove stale comment * handle feedback * Add TransferOptions --- chain/cosmos/chain_node.go | 21 +- chain/cosmos/cosmos_chain.go | 10 +- chain/cosmos/poll.go | 65 ++- chain/penumbra/penumbra_app_node.go | 8 +- chain/penumbra/penumbra_chain.go | 10 +- chain/polkadot/polkadot_chain.go | 8 +- conformance/flush.go | 2 +- conformance/test.go | 4 +- docs/writeCustomTests.md | 7 +- examples/cosmos/light_client_test.go | 100 ++++ examples/ibc/learn_ibc_test.go | 7 +- examples/ibc/packet_forward_test.go | 642 ++++++++++++++++++++----- ibc/chain.go | 8 +- ibc/relayer.go | 79 +++ ibc/types.go | 11 + interchain_test.go | 11 +- internal/blockdb/messages_view_test.go | 5 +- relayer/docker.go | 15 + relayer/rly/cosmos_relayer.go | 29 ++ 19 files changed, 900 insertions(+), 142 deletions(-) create mode 100644 examples/cosmos/light_client_test.go diff --git a/chain/cosmos/chain_node.go b/chain/cosmos/chain_node.go index 6bd018db3..8e13dc7be 100644 --- a/chain/cosmos/chain_node.go +++ b/chain/cosmos/chain_node.go @@ -584,18 +584,27 @@ type CosmosTx struct { RawLog string `json:"raw_log"` } -func (tn *ChainNode) SendIBCTransfer(ctx context.Context, channelID string, keyName string, amount ibc.WalletAmount, timeout *ibc.IBCTimeout) (string, error) { +func (tn *ChainNode) SendIBCTransfer( + ctx context.Context, + channelID string, + keyName string, + amount ibc.WalletAmount, + options ibc.TransferOptions, +) (string, error) { command := []string{ "ibc-transfer", "transfer", "transfer", channelID, amount.Address, fmt.Sprintf("%d%s", amount.Amount, amount.Denom), } - if timeout != nil { - if timeout.NanoSeconds > 0 { - command = append(command, "--packet-timeout-timestamp", fmt.Sprint(timeout.NanoSeconds)) - } else if timeout.Height > 0 { - command = append(command, "--packet-timeout-height", fmt.Sprintf("0-%d", timeout.Height)) + if options.Timeout != nil { + if options.Timeout.NanoSeconds > 0 { + command = append(command, "--packet-timeout-timestamp", fmt.Sprint(options.Timeout.NanoSeconds)) + } else if options.Timeout.Height > 0 { + command = append(command, "--packet-timeout-height", fmt.Sprintf("0-%d", options.Timeout.Height)) } } + if options.Memo != "" { + command = append(command, "--memo", options.Memo) + } return tn.ExecTx(ctx, keyName, command...) } diff --git a/chain/cosmos/cosmos_chain.go b/chain/cosmos/cosmos_chain.go index 1ba9b535e..f1a69d865 100644 --- a/chain/cosmos/cosmos_chain.go +++ b/chain/cosmos/cosmos_chain.go @@ -228,8 +228,14 @@ func (c *CosmosChain) SendFunds(ctx context.Context, keyName string, amount ibc. } // Implements Chain interface -func (c *CosmosChain) SendIBCTransfer(ctx context.Context, channelID, keyName string, amount ibc.WalletAmount, timeout *ibc.IBCTimeout) (tx ibc.Tx, _ error) { - txHash, err := c.getFullNode().SendIBCTransfer(ctx, channelID, keyName, amount, timeout) +func (c *CosmosChain) SendIBCTransfer( + ctx context.Context, + channelID string, + keyName string, + amount ibc.WalletAmount, + options ibc.TransferOptions, +) (tx ibc.Tx, _ error) { + txHash, err := c.getFullNode().SendIBCTransfer(ctx, channelID, keyName, amount, options) if err != nil { return tx, fmt.Errorf("send ibc transfer: %w", err) } diff --git a/chain/cosmos/poll.go b/chain/cosmos/poll.go index ee385a80d..8be2678e1 100644 --- a/chain/cosmos/poll.go +++ b/chain/cosmos/poll.go @@ -2,15 +2,18 @@ package cosmos import ( "context" + "errors" "fmt" - "github.com/strangelove-ventures/ibctest/v3/test" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/strangelove-ventures/ibctest/v3/ibc" + "github.com/strangelove-ventures/ibctest/v3/testutil" ) // PollForProposalStatus attempts to find a proposal with matching ID and status. func PollForProposalStatus(ctx context.Context, chain *CosmosChain, startHeight, maxHeight uint64, proposalID string, status string) (ProposalResponse, error) { var zero ProposalResponse - doPoll := func(ctx context.Context, height uint64) (any, error) { + doPoll := func(ctx context.Context, height uint64) (ProposalResponse, error) { p, err := chain.QueryProposal(ctx, proposalID) if err != nil { return zero, err @@ -20,10 +23,60 @@ func PollForProposalStatus(ctx context.Context, chain *CosmosChain, startHeight, } return *p, nil } - bp := test.BlockPoller{CurrentHeight: chain.Height, PollFunc: doPoll} - p, err := bp.DoPoll(ctx, startHeight, maxHeight) + bp := testutil.BlockPoller[ProposalResponse]{CurrentHeight: chain.Height, PollFunc: doPoll} + return bp.DoPoll(ctx, startHeight, maxHeight) +} + +// PollForMessage searches every transaction for a message. Must pass a coded registry capable of decoding the cosmos transaction. +// fn is optional. Return true from the fn to stop polling and return the found message. If fn is nil, returns the first message to match type T. +func PollForMessage[T any](ctx context.Context, chain *CosmosChain, registry codectypes.InterfaceRegistry, startHeight, maxHeight uint64, fn func(found T) bool) (T, error) { + var zero T + if fn == nil { + fn = func(T) bool { return true } + } + doPoll := func(ctx context.Context, height uint64) (T, error) { + h := int64(height) + block, err := chain.getFullNode().Client.Block(ctx, &h) + if err != nil { + return zero, err + } + for _, tx := range block.Block.Txs { + sdkTx, err := decodeTX(registry, tx) + if err != nil { + return zero, err + } + for _, msg := range sdkTx.GetMsgs() { + if found, ok := msg.(T); ok { + if fn(found) { + return found, nil + } + } + } + } + return zero, errors.New("not found") + } + + bp := testutil.BlockPoller[T]{CurrentHeight: chain.Height, PollFunc: doPoll} + return bp.DoPoll(ctx, startHeight, maxHeight) +} + +// 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 zero, err + 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 } - return p.(ProposalResponse), nil + bp := testutil.BlockPoller[any]{CurrentHeight: chain.Height, PollFunc: doPoll} + _, err = bp.DoPoll(ctx, h, h+deltaBlocks) + return err } diff --git a/chain/penumbra/penumbra_app_node.go b/chain/penumbra/penumbra_app_node.go index 82b6feba5..5c09c4f3a 100644 --- a/chain/penumbra/penumbra_app_node.go +++ b/chain/penumbra/penumbra_app_node.go @@ -187,7 +187,13 @@ func (p *PenumbraAppNode) SendFunds(ctx context.Context, keyName string, amount return errors.New("not yet implemented") } -func (p *PenumbraAppNode) SendIBCTransfer(ctx context.Context, channelID, keyName string, amount ibc.WalletAmount, timeout *ibc.IBCTimeout) (ibc.Tx, error) { +func (p *PenumbraAppNode) SendIBCTransfer( + ctx context.Context, + channelID string, + keyName string, + amount ibc.WalletAmount, + options ibc.TransferOptions, +) (ibc.Tx, error) { return ibc.Tx{}, errors.New("not yet implemented") } diff --git a/chain/penumbra/penumbra_chain.go b/chain/penumbra/penumbra_chain.go index 395411845..6ec14ccc1 100644 --- a/chain/penumbra/penumbra_chain.go +++ b/chain/penumbra/penumbra_chain.go @@ -145,8 +145,14 @@ func (c *PenumbraChain) SendFunds(ctx context.Context, keyName string, amount ib } // Implements Chain interface -func (c *PenumbraChain) SendIBCTransfer(ctx context.Context, channelID, keyName string, amount ibc.WalletAmount, timeout *ibc.IBCTimeout) (ibc.Tx, error) { - return c.getRelayerNode().PenumbraAppNode.SendIBCTransfer(ctx, channelID, keyName, amount, timeout) +func (c *PenumbraChain) SendIBCTransfer( + ctx context.Context, + channelID string, + keyName string, + amount ibc.WalletAmount, + options ibc.TransferOptions, +) (ibc.Tx, error) { + return c.getRelayerNode().PenumbraAppNode.SendIBCTransfer(ctx, channelID, keyName, amount, options) } // Implements Chain interface diff --git a/chain/polkadot/polkadot_chain.go b/chain/polkadot/polkadot_chain.go index 2639e3816..90aa5265b 100644 --- a/chain/polkadot/polkadot_chain.go +++ b/chain/polkadot/polkadot_chain.go @@ -538,7 +538,13 @@ func (c *PolkadotChain) SendFunds(ctx context.Context, keyName string, amount ib // SendIBCTransfer sends an IBC transfer returning a transaction or an error if the transfer failed. // Implements Chain interface. -func (c *PolkadotChain) SendIBCTransfer(ctx context.Context, channelID, keyName string, amount ibc.WalletAmount, timeout *ibc.IBCTimeout) (ibc.Tx, error) { +func (c *PolkadotChain) SendIBCTransfer( + ctx context.Context, + channelID string, + keyName string, + amount ibc.WalletAmount, + options ibc.TransferOptions, +) (ibc.Tx, error) { panic("not implemented yet") } diff --git a/conformance/flush.go b/conformance/flush.go index f397f52d0..d8db44db9 100644 --- a/conformance/flush.go +++ b/conformance/flush.go @@ -79,7 +79,7 @@ func TestRelayerFlushing(t *testing.T, ctx context.Context, cf ibctest.ChainFact Address: c1FaucetAddr, Denom: c0.Config().Denom, Amount: txAmount, - }, nil) + }, ibc.TransferOptions{}) req.NoError(err) req.NoError(tx.Validate()) diff --git a/conformance/test.go b/conformance/test.go index b1473c532..20ce0ef3c 100644 --- a/conformance/test.go +++ b/conformance/test.go @@ -166,7 +166,7 @@ func sendIBCTransfersFromBothChainsWithTimeout( eg.Go(func() (err error) { for i, channel := range channels { srcChannelID := channel.ChannelID - srcTxs[i], err = srcChain.SendIBCTransfer(ctx, srcChannelID, srcUser.KeyName, testCoinSrcToDst, timeout) + srcTxs[i], err = srcChain.SendIBCTransfer(ctx, srcChannelID, srcUser.KeyName, testCoinSrcToDst, ibc.TransferOptions{Timeout: timeout}) if err != nil { return fmt.Errorf("failed to send ibc transfer from source: %w", err) } @@ -180,7 +180,7 @@ func sendIBCTransfersFromBothChainsWithTimeout( eg.Go(func() (err error) { for i, channel := range channels { dstChannelID := channel.Counterparty.ChannelID - dstTxs[i], err = dstChain.SendIBCTransfer(ctx, dstChannelID, dstUser.KeyName, testCoinDstToSrc, timeout) + dstTxs[i], err = dstChain.SendIBCTransfer(ctx, dstChannelID, dstUser.KeyName, testCoinDstToSrc, ibc.TransferOptions{Timeout: timeout}) if err != nil { return fmt.Errorf("failed to send ibc transfer from destination: %w", err) } diff --git a/docs/writeCustomTests.md b/docs/writeCustomTests.md index 63a5ad50f..3f38d493e 100644 --- a/docs/writeCustomTests.md +++ b/docs/writeCustomTests.md @@ -216,13 +216,12 @@ osmosisRPC := osmosis.GetGRPCAddress() Here we send an IBC Transaction: ```go amountToSend := int64(1_000_000) -tx, err := gaia.SendIBCTransfer(ctx, gaiaChannelID, gaiaUser.KeyName, ibc.WalletAmount{ +transfer := ibc.WalletAmount{ Address: osmosisUser.Bech32Address(osmosis.Config().Bech32Prefix), Denom: gaia.Config().Denom, Amount: amountToSend, -}, - nil, -) +} +tx, err := gaia.SendIBCTransfer(ctx, gaiaChannelID, gaiaUser.KeyName, transfer, ibc.TransferOptions{}) ``` The `Exec` method allows any arbitrary command to be passed into a chain binary or relayer binary. diff --git a/examples/cosmos/light_client_test.go b/examples/cosmos/light_client_test.go new file mode 100644 index 000000000..f5a74c668 --- /dev/null +++ b/examples/cosmos/light_client_test.go @@ -0,0 +1,100 @@ +package cosmos_test + +import ( + "context" + "testing" + + clienttypes "github.com/cosmos/ibc-go/v3/modules/core/02-client/types" + ibctest "github.com/strangelove-ventures/ibctest/v3" + "github.com/strangelove-ventures/ibctest/v3/chain/cosmos" + "github.com/strangelove-ventures/ibctest/v3/ibc" + "github.com/strangelove-ventures/ibctest/v3/testreporter" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" +) + +func TestUpdateLightClients(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + + t.Parallel() + + ctx := context.Background() + + // Chains + cf := ibctest.NewBuiltinChainFactory(zaptest.NewLogger(t), []*ibctest.ChainSpec{ + {Name: "gaia", Version: gaiaVersion}, + {Name: "osmosis", Version: osmosisVersion}, + }) + + chains, err := cf.Chains(t.Name()) + require.NoError(t, err) + gaia, osmosis := chains[0], chains[1] + + // Relayer + client, network := ibctest.DockerSetup(t) + r := ibctest.NewBuiltinRelayerFactory(ibc.CosmosRly, zaptest.NewLogger(t)).Build( + t, client, network) + + ic := ibctest.NewInterchain(). + AddChain(gaia). + AddChain(osmosis). + AddRelayer(r, "relayer"). + AddLink(ibctest.InterchainLink{ + Chain1: gaia, + Chain2: osmosis, + Relayer: r, + Path: "client-test-path", + }) + + // Build interchain + rep := testreporter.NewNopReporter() + eRep := rep.RelayerExecReporter(t) + require.NoError(t, ic.Build(ctx, eRep, ibctest.InterchainBuildOptions{ + TestName: t.Name(), + Client: client, + NetworkID: network, + })) + t.Cleanup(func() { + _ = ic.Close() + }) + + require.NoError(t, r.StartRelayer(ctx, eRep)) + t.Cleanup(func() { + _ = r.StopRelayer(ctx, eRep) + }) + + // Create and Fund User Wallets + fundAmount := int64(10_000_000) + users := ibctest.GetAndFundTestUsers(t, ctx, "default", fundAmount, gaia, osmosis) + gaiaUser, osmoUser := users[0], users[1] + + // Get Channel ID + gaiaChannelInfo, err := r.GetChannels(ctx, eRep, gaia.Config().ChainID) + require.NoError(t, err) + chanID := gaiaChannelInfo[0].ChannelID + + height, err := osmosis.Height(ctx) + require.NoError(t, err) + + amountToSend := int64(553255) // Unique amount to make log searching easier. + dstAddress := osmoUser.Bech32Address(osmosis.Config().Bech32Prefix) + transfer := ibc.WalletAmount{ + Address: dstAddress, + Denom: gaia.Config().Denom, + Amount: amountToSend, + } + tx, err := gaia.SendIBCTransfer(ctx, chanID, gaiaUser.KeyName, transfer, ibc.TransferOptions{}) + require.NoError(t, err) + require.NoError(t, tx.Validate()) + + chain := osmosis.(*cosmos.CosmosChain) + reg := chain.Config().EncodingConfig.InterfaceRegistry + msg, err := cosmos.PollForMessage[*clienttypes.MsgUpdateClient](ctx, chain, reg, height, height+10, nil) + require.NoError(t, err) + + require.Equal(t, "07-tendermint-0", msg.ClientId) + require.NotEmpty(t, msg.Signer) + // TODO: Assert header information +} diff --git a/examples/ibc/learn_ibc_test.go b/examples/ibc/learn_ibc_test.go index 23644b8a2..ae9f3d3de 100644 --- a/examples/ibc/learn_ibc_test.go +++ b/examples/ibc/learn_ibc_test.go @@ -89,13 +89,12 @@ func TestLearn(t *testing.T) { // Send Transaction amountToSend := int64(1_000_000) dstAddress := osmosisUser.Bech32Address(osmosis.Config().Bech32Prefix) - tx, err := gaia.SendIBCTransfer(ctx, gaiaChannelID, gaiaUser.KeyName, ibc.WalletAmount{ + transfer := ibc.WalletAmount{ Address: dstAddress, Denom: gaia.Config().Denom, Amount: amountToSend, - }, - nil, - ) + } + tx, err := gaia.SendIBCTransfer(ctx, gaiaChannelID, gaiaUser.KeyName, transfer, ibc.TransferOptions{}) require.NoError(t, err) require.NoError(t, tx.Validate()) diff --git a/examples/ibc/packet_forward_test.go b/examples/ibc/packet_forward_test.go index f80a64620..2ca2d1f8a 100644 --- a/examples/ibc/packet_forward_test.go +++ b/examples/ibc/packet_forward_test.go @@ -2,21 +2,39 @@ package ibc_test import ( "context" - "fmt" + "encoding/json" "testing" + "time" transfertypes "github.com/cosmos/ibc-go/v3/modules/apps/transfer/types" - "github.com/strangelove-ventures/ibctest/v3" + ibctest "github.com/strangelove-ventures/ibctest/v3" + "github.com/strangelove-ventures/ibctest/v3/chain/cosmos" "github.com/strangelove-ventures/ibctest/v3/ibc" - "github.com/strangelove-ventures/ibctest/v3/test" + "github.com/strangelove-ventures/ibctest/v3/relayer" + "github.com/strangelove-ventures/ibctest/v3/relayer/rly" "github.com/strangelove-ventures/ibctest/v3/testreporter" + "github.com/strangelove-ventures/ibctest/v3/testutil" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" ) +type PacketMetadata struct { + Forward *ForwardMetadata `json:"forward"` +} + +type ForwardMetadata struct { + Receiver string `json:"receiver"` + Port string `json:"port"` + Channel string `json:"channel"` + Timeout time.Duration `json:"timeout"` + Retries *uint8 `json:"retries,omitempty"` + Next *string `json:"next,omitempty"` + RefundSequence *uint64 `json:"refund_sequence,omitempty"` +} + func TestPacketForwardMiddleware(t *testing.T) { if testing.Short() { - t.Skip() + t.Skip("skipping in short mode") } client, network := ibctest.DockerSetup(t) @@ -26,43 +44,54 @@ func TestPacketForwardMiddleware(t *testing.T) { ctx := context.Background() + chainID_A, chainID_B, chainID_C, chainID_D := "chain-a", "chain-b", "chain-c", "chain-d" + 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", Version: "strangelove-forward_middleware_memo_v3", ChainConfig: ibc.ChainConfig{ChainID: chainID_A, GasPrices: "0.0uatom"}}, + {Name: "gaia", Version: "strangelove-forward_middleware_memo_v3", ChainConfig: ibc.ChainConfig{ChainID: chainID_B, GasPrices: "0.0uatom"}}, + {Name: "gaia", Version: "strangelove-forward_middleware_memo_v3", ChainConfig: ibc.ChainConfig{ChainID: chainID_C, GasPrices: "0.0uatom"}}, + {Name: "gaia", Version: "strangelove-forward_middleware_memo_v3", ChainConfig: ibc.ChainConfig{ChainID: chainID_D, GasPrices: "0.0uatom"}}, }) chains, err := cf.Chains(t.Name()) require.NoError(t, err) - gaia, osmosis, juno := chains[0], chains[1], chains[2] + chainA, chainB, chainC, chainD := chains[0].(*cosmos.CosmosChain), chains[1].(*cosmos.CosmosChain), chains[2].(*cosmos.CosmosChain), chains[3].(*cosmos.CosmosChain) r := ibctest.NewBuiltinRelayerFactory( ibc.CosmosRly, zaptest.NewLogger(t), - ).Build( - t, client, network, - ) + // TODO remove this line once default rly version includes https://github.com/cosmos/relayer/pull/1038 + relayer.CustomDockerImage("ghcr.io/cosmos/relayer", "main", rly.RlyDefaultUidGid), + ).Build(t, client, network) - const pathOsmoHub = "osmohub" - const pathJunoHub = "junohub" + const pathAB = "ab" + const pathBC = "bc" + const pathCD = "cd" ic := ibctest.NewInterchain(). - AddChain(osmosis). - AddChain(gaia). - AddChain(juno). + AddChain(chainA). + AddChain(chainB). + AddChain(chainC). + AddChain(chainD). AddRelayer(r, "relayer"). AddLink(ibctest.InterchainLink{ - Chain1: osmosis, - Chain2: gaia, + Chain1: chainA, + Chain2: chainB, Relayer: r, - Path: pathOsmoHub, + Path: pathAB, }). AddLink(ibctest.InterchainLink{ - Chain1: gaia, - Chain2: juno, + Chain1: chainB, + Chain2: chainC, Relayer: r, - Path: pathJunoHub, + Path: pathBC, + }). + AddLink(ibctest.InterchainLink{ + Chain1: chainC, + Chain2: chainD, + Relayer: r, + Path: pathCD, }) require.NoError(t, ic.Build(ctx, eRep, ibctest.InterchainBuildOptions{ @@ -78,16 +107,25 @@ func TestPacketForwardMiddleware(t *testing.T) { }) const userFunds = int64(10_000_000_000) - users := ibctest.GetAndFundTestUsers(t, ctx, t.Name(), userFunds, osmosis, gaia, juno) + users := ibctest.GetAndFundTestUsers(t, ctx, t.Name(), userFunds, chainA, chainB, chainC, chainD) + + abChan, err := ibc.GetTransferChannel(ctx, r, eRep, chainID_A, chainID_B) + require.NoError(t, err) - osmoChannels, err := r.GetChannels(ctx, eRep, osmosis.Config().ChainID) + baChan := abChan.Counterparty + + cbChan, err := ibc.GetTransferChannel(ctx, r, eRep, chainID_C, chainID_B) require.NoError(t, err) - junoChannels, err := r.GetChannels(ctx, eRep, juno.Config().ChainID) + bcChan := cbChan.Counterparty + + dcChan, err := ibc.GetTransferChannel(ctx, r, eRep, chainID_D, chainID_C) require.NoError(t, err) + cdChan := dcChan.Counterparty + // Start the relayer on both paths - err = r.StartRelayer(ctx, eRep, pathOsmoHub, pathJunoHub) + err = r.StartRelayer(ctx, eRep, pathAB, pathBC, pathCD) require.NoError(t, err) t.Cleanup( @@ -100,96 +138,482 @@ func TestPacketForwardMiddleware(t *testing.T) { ) // Get original account balances - osmosisUser, gaiaUser, junoUser := users[0], users[1], users[2] + userA, userB, userC, userD := users[0], users[1], users[2], users[3] - 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 - gaiaJunoChan := junoChannels[0].Counterparty - receiver := fmt.Sprintf("%s|%s/%s:%s", gaiaUser.Bech32Address(gaia.Config().Bech32Prefix), gaiaJunoChan.PortID, gaiaJunoChan.ChannelID, junoUser.Bech32Address(juno.Config().Bech32Prefix)) - transfer := ibc.WalletAmount{ - Address: receiver, - Denom: osmosis.Config().Denom, - Amount: transferAmount, - } - - osmosisGaiaChan := osmoChannels[0] - _, 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] - firstHopDenom := transfertypes.GetPrefixedDenom(gaiaOsmoChan.PortID, gaiaOsmoChan.ChannelID, osmosis.Config().Denom) - secondHopDenom := transfertypes.GetPrefixedDenom(junoGaiaChan.PortID, junoGaiaChan.ChannelID, firstHopDenom) - dstIbcDenom := transfertypes.ParseDenomTrace(secondHopDenom) - - // Check that the funds sent are present in the acc on juno - junoBal, err := juno.GetBalance(ctx, junoUser.Bech32Address(juno.Config().Bech32Prefix), dstIbcDenom.IBCDenom()) - 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)) - transfer = ibc.WalletAmount{ - Address: receiver, - Denom: dstIbcDenom.IBCDenom(), - Amount: transferAmount, - } - - _, 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()) - require.NoError(t, err) - require.Equal(t, int64(0), junoBal) + firstHopDenom := transfertypes.GetPrefixedDenom(baChan.PortID, baChan.ChannelID, chainA.Config().Denom) + secondHopDenom := transfertypes.GetPrefixedDenom(cbChan.PortID, cbChan.ChannelID, firstHopDenom) + thirdHopDenom := transfertypes.GetPrefixedDenom(dcChan.PortID, dcChan.ChannelID, secondHopDenom) + + firstHopDenomTrace := transfertypes.ParseDenomTrace(firstHopDenom) + secondHopDenomTrace := transfertypes.ParseDenomTrace(secondHopDenom) + thirdHopDenomTrace := transfertypes.ParseDenomTrace(thirdHopDenom) + + firstHopIBCDenom := firstHopDenomTrace.IBCDenom() + secondHopIBCDenom := secondHopDenomTrace.IBCDenom() + thirdHopIBCDenom := thirdHopDenomTrace.IBCDenom() + + firstHopEscrowAccount := transfertypes.GetEscrowAddress(abChan.PortID, abChan.ChannelID).String() + secondHopEscrowAccount := transfertypes.GetEscrowAddress(bcChan.PortID, bcChan.ChannelID).String() + thirdHopEscrowAccount := transfertypes.GetEscrowAddress(cdChan.PortID, abChan.ChannelID).String() + + t.Run("multi-hop a->b->c->d", func(t *testing.T) { + // Send packet from Chain A->Chain B->Chain C->Chain D + + transfer := ibc.WalletAmount{ + Address: userB.Bech32Address(chainB.Config().Bech32Prefix), + Denom: chainA.Config().Denom, + Amount: transferAmount, + } + + secondHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userD.Bech32Address(chainD.Config().Bech32Prefix), + Channel: cdChan.ChannelID, + Port: cdChan.PortID, + }, + } + nextBz, err := json.Marshal(secondHopMetadata) + require.NoError(t, err) + next := string(nextBz) + + firstHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userC.Bech32Address(chainC.Config().Bech32Prefix), + Channel: bcChan.ChannelID, + Port: bcChan.PortID, + Next: &next, + }, + } + + memo, err := json.Marshal(firstHopMetadata) + require.NoError(t, err) + + chainAHeight, err := chainA.Height(ctx) + require.NoError(t, err) + + transferTx, err := chainA.SendIBCTransfer(ctx, abChan.ChannelID, userA.KeyName, transfer, ibc.TransferOptions{Memo: string(memo)}) + require.NoError(t, err) + _, err = testutil.PollForAck(ctx, chainA, chainAHeight, chainAHeight+30, transferTx.Packet) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 1, chainA) + require.NoError(t, err) + + chainABalance, err := chainA.GetBalance(ctx, userA.Bech32Address(chainA.Config().Bech32Prefix), chainA.Config().Denom) + require.NoError(t, err) + + chainBBalance, err := chainB.GetBalance(ctx, userB.Bech32Address(chainB.Config().Bech32Prefix), firstHopIBCDenom) + require.NoError(t, err) + + chainCBalance, err := chainC.GetBalance(ctx, userC.Bech32Address(chainC.Config().Bech32Prefix), secondHopIBCDenom) + require.NoError(t, err) + + chainDBalance, err := chainD.GetBalance(ctx, userD.Bech32Address(chainD.Config().Bech32Prefix), thirdHopIBCDenom) + require.NoError(t, err) + + require.Equal(t, userFunds-transferAmount, chainABalance) + require.Equal(t, int64(0), chainBBalance) + require.Equal(t, int64(0), chainCBalance) + require.Equal(t, transferAmount, chainDBalance) + + firstHopEscrowBalance, err := chainA.GetBalance(ctx, firstHopEscrowAccount, chainA.Config().Denom) + require.NoError(t, err) + + secondHopEscrowBalance, err := chainB.GetBalance(ctx, secondHopEscrowAccount, firstHopIBCDenom) + require.NoError(t, err) + + thirdHopEscrowBalance, err := chainC.GetBalance(ctx, thirdHopEscrowAccount, secondHopIBCDenom) + require.NoError(t, err) + + require.Equal(t, transferAmount, firstHopEscrowBalance) + require.Equal(t, transferAmount, secondHopEscrowBalance) + require.Equal(t, transferAmount, thirdHopEscrowBalance) + }) - // 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) - 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. - receiver = fmt.Sprintf("%s|%s/%s:%s", gaiaUser.Bech32Address(gaia.Config().Bech32Prefix), gaiaJunoChan.PortID, gaiaJunoChan.ChannelID, "xyz1t8eh66t2w5k67kwurmn5gqhtq6d2ja0vp7jmmq") - transfer = ibc.WalletAmount{ - Address: receiver, - Denom: osmosis.Config().Denom, - Amount: transferAmount, - } + t.Run("multi-hop denom unwind d->c->b->a", func(t *testing.T) { + // Send packet back from Chain D->Chain C->Chain B->Chain A + transfer := ibc.WalletAmount{ + Address: userC.Bech32Address(chainC.Config().Bech32Prefix), + Denom: thirdHopIBCDenom, + Amount: transferAmount, + } + + secondHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userA.Bech32Address(chainA.Config().Bech32Prefix), + Channel: baChan.ChannelID, + Port: baChan.PortID, + }, + } + + nextBz, err := json.Marshal(secondHopMetadata) + require.NoError(t, err) + + next := string(nextBz) + + firstHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userB.Bech32Address(chainB.Config().Bech32Prefix), + Channel: cbChan.ChannelID, + Port: cbChan.PortID, + Next: &next, + }, + } + + memo, err := json.Marshal(firstHopMetadata) + require.NoError(t, err) + + chainDHeight, err := chainD.Height(ctx) + require.NoError(t, err) + + transferTx, err := chainD.SendIBCTransfer(ctx, dcChan.ChannelID, userD.KeyName, transfer, ibc.TransferOptions{Memo: string(memo)}) + require.NoError(t, err) + _, err = testutil.PollForAck(ctx, chainD, chainDHeight, chainDHeight+30, transferTx.Packet) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 1, chainA) + require.NoError(t, err) + + // assert balances for user controlled wallets + chainDBalance, err := chainD.GetBalance(ctx, userD.Bech32Address(chainD.Config().Bech32Prefix), thirdHopIBCDenom) + require.NoError(t, err) + + chainCBalance, err := chainC.GetBalance(ctx, userC.Bech32Address(chainC.Config().Bech32Prefix), secondHopIBCDenom) + require.NoError(t, err) + + chainBBalance, err := chainB.GetBalance(ctx, userB.Bech32Address(chainB.Config().Bech32Prefix), firstHopIBCDenom) + require.NoError(t, err) + + chainABalance, err := chainA.GetBalance(ctx, userA.Bech32Address(chainA.Config().Bech32Prefix), chainA.Config().Denom) + require.NoError(t, err) + + require.Equal(t, int64(0), chainDBalance) + require.Equal(t, int64(0), chainCBalance) + require.Equal(t, int64(0), chainBBalance) + require.Equal(t, userFunds, chainABalance) + + // assert balances for IBC escrow accounts + firstHopEscrowBalance, err := chainA.GetBalance(ctx, firstHopEscrowAccount, chainA.Config().Denom) + require.NoError(t, err) + + secondHopEscrowBalance, err := chainB.GetBalance(ctx, secondHopEscrowAccount, firstHopIBCDenom) + require.NoError(t, err) + + thirdHopEscrowBalance, err := chainC.GetBalance(ctx, thirdHopEscrowAccount, secondHopIBCDenom) + require.NoError(t, err) + + require.Equal(t, int64(0), firstHopEscrowBalance) + require.Equal(t, int64(0), secondHopEscrowBalance) + require.Equal(t, int64(0), thirdHopEscrowBalance) + }) - _, err = osmosis.SendIBCTransfer(ctx, osmosisGaiaChan.ChannelID, osmosisUser.KeyName, transfer, nil) - require.NoError(t, err) + t.Run("forward ack error refund", func(t *testing.T) { + // Send a malformed packet with invalid receiver address from Chain A->Chain B->Chain C + // This should succeed in the first hop and fail to make the second hop; funds should then be refunded to Chain A. + transfer := ibc.WalletAmount{ + Address: userB.Bech32Address(chainB.Config().Bech32Prefix), + Denom: chainA.Config().Denom, + Amount: transferAmount, + } + + metadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: "xyz1t8eh66t2w5k67kwurmn5gqhtq6d2ja0vp7jmmq", // malformed receiver address on Chain C + Channel: bcChan.ChannelID, + Port: bcChan.PortID, + }, + } + + memo, err := json.Marshal(metadata) + require.NoError(t, err) + + chainAHeight, err := chainA.Height(ctx) + require.NoError(t, err) + + transferTx, err := chainA.SendIBCTransfer(ctx, abChan.ChannelID, userA.KeyName, transfer, ibc.TransferOptions{Memo: string(memo)}) + require.NoError(t, err) + _, err = testutil.PollForAck(ctx, chainA, chainAHeight, chainAHeight+25, transferTx.Packet) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 1, chainA) + require.NoError(t, err) + + // assert balances for user controlled wallets + chainABalance, err := chainA.GetBalance(ctx, userA.Bech32Address(chainA.Config().Bech32Prefix), chainA.Config().Denom) + require.NoError(t, err) + + chainBBalance, err := chainB.GetBalance(ctx, userB.Bech32Address(chainB.Config().Bech32Prefix), firstHopIBCDenom) + require.NoError(t, err) + + chainCBalance, err := chainC.GetBalance(ctx, userC.Bech32Address(chainC.Config().Bech32Prefix), secondHopIBCDenom) + require.NoError(t, err) + + require.Equal(t, userFunds, chainABalance) + require.Equal(t, int64(0), chainBBalance) + require.Equal(t, int64(0), chainCBalance) + + // assert balances for IBC escrow accounts + firstHopEscrowBalance, err := chainA.GetBalance(ctx, firstHopEscrowAccount, chainA.Config().Denom) + require.NoError(t, err) + + secondHopEscrowBalance, err := chainB.GetBalance(ctx, secondHopEscrowAccount, firstHopIBCDenom) + require.NoError(t, err) + + require.Equal(t, int64(0), firstHopEscrowBalance) + require.Equal(t, int64(0), secondHopEscrowBalance) + }) - // Wait for transfer to be relayed - err = test.WaitForBlocks(ctx, 10, gaia) - require.NoError(t, err) + t.Run("forward timeout refund", func(t *testing.T) { + // Send packet from Chain A->Chain B->Chain C with the timeout so low for B->C transfer that it can not make it from B to C, which should result in a refund from B to A after two retries. + transfer := ibc.WalletAmount{ + Address: userB.Bech32Address(chainB.Config().Bech32Prefix), + Denom: chainA.Config().Denom, + Amount: transferAmount, + } + + retries := uint8(2) + metadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userC.Bech32Address(chainC.Config().Bech32Prefix), + Channel: bcChan.ChannelID, + Port: bcChan.PortID, + Retries: &retries, + Timeout: 1 * time.Second, + }, + } + + memo, err := json.Marshal(metadata) + require.NoError(t, err) + + chainAHeight, err := chainA.Height(ctx) + require.NoError(t, err) + + transferTx, err := chainA.SendIBCTransfer(ctx, abChan.ChannelID, userA.KeyName, transfer, ibc.TransferOptions{Memo: string(memo)}) + require.NoError(t, err) + _, err = testutil.PollForAck(ctx, chainA, chainAHeight, chainAHeight+25, transferTx.Packet) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 1, chainA) + require.NoError(t, err) + + // assert balances for user controlled wallets + chainABalance, err := chainA.GetBalance(ctx, userA.Bech32Address(chainA.Config().Bech32Prefix), chainA.Config().Denom) + require.NoError(t, err) + + chainBBalance, err := chainB.GetBalance(ctx, userB.Bech32Address(chainB.Config().Bech32Prefix), firstHopIBCDenom) + require.NoError(t, err) + + chainCBalance, err := chainC.GetBalance(ctx, userC.Bech32Address(chainC.Config().Bech32Prefix), secondHopIBCDenom) + require.NoError(t, err) + + require.Equal(t, userFunds, chainABalance) + require.Equal(t, int64(0), chainBBalance) + require.Equal(t, int64(0), chainCBalance) + + firstHopEscrowBalance, err := chainA.GetBalance(ctx, firstHopEscrowAccount, chainA.Config().Denom) + require.NoError(t, err) + + secondHopEscrowBalance, err := chainB.GetBalance(ctx, secondHopEscrowAccount, firstHopIBCDenom) + require.NoError(t, err) + + require.Equal(t, int64(0), firstHopEscrowBalance) + require.Equal(t, int64(0), secondHopEscrowBalance) + }) - // 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) + t.Run("multi-hop ack error refund", func(t *testing.T) { + // Send a malformed packet with invalid receiver address from Chain A->Chain B->Chain C->Chain D + // This should succeed in the first hop and second hop, then fail to make the third hop. + // Funds should be refunded to Chain B and then to Chain A via acknowledgements with errors. + transfer := ibc.WalletAmount{ + Address: userB.Bech32Address(chainB.Config().Bech32Prefix), + Denom: chainA.Config().Denom, + Amount: transferAmount, + } + + secondHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: "xyz1t8eh66t2w5k67kwurmn5gqhtq6d2ja0vp7jmmq", // malformed receiver address on chain D + Channel: cdChan.ChannelID, + Port: cdChan.PortID, + }, + } + + nextBz, err := json.Marshal(secondHopMetadata) + require.NoError(t, err) + + next := string(nextBz) + + firstHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userC.Bech32Address(chainC.Config().Bech32Prefix), + Channel: bcChan.ChannelID, + Port: bcChan.PortID, + Next: &next, + }, + } + + memo, err := json.Marshal(firstHopMetadata) + require.NoError(t, err) + + chainAHeight, err := chainA.Height(ctx) + require.NoError(t, err) + + transferTx, err := chainA.SendIBCTransfer(ctx, abChan.ChannelID, userA.KeyName, transfer, ibc.TransferOptions{Memo: string(memo)}) + require.NoError(t, err) + _, err = testutil.PollForAck(ctx, chainA, chainAHeight, chainAHeight+30, transferTx.Packet) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 1, chainA) + require.NoError(t, err) + + // assert balances for user controlled wallets + chainDBalance, err := chainD.GetBalance(ctx, userD.Bech32Address(chainD.Config().Bech32Prefix), thirdHopIBCDenom) + require.NoError(t, err) + + chainCBalance, err := chainC.GetBalance(ctx, userC.Bech32Address(chainC.Config().Bech32Prefix), secondHopIBCDenom) + require.NoError(t, err) + + chainBBalance, err := chainB.GetBalance(ctx, userB.Bech32Address(chainB.Config().Bech32Prefix), firstHopIBCDenom) + require.NoError(t, err) + + chainABalance, err := chainA.GetBalance(ctx, userA.Bech32Address(chainA.Config().Bech32Prefix), chainA.Config().Denom) + require.NoError(t, err) + + require.Equal(t, userFunds, chainABalance) + require.Equal(t, int64(0), chainBBalance) + require.Equal(t, int64(0), chainCBalance) + require.Equal(t, int64(0), chainDBalance) + + // assert balances for IBC escrow accounts + firstHopEscrowBalance, err := chainA.GetBalance(ctx, firstHopEscrowAccount, chainA.Config().Denom) + require.NoError(t, err) + + secondHopEscrowBalance, err := chainB.GetBalance(ctx, secondHopEscrowAccount, firstHopIBCDenom) + require.NoError(t, err) + + thirdHopEscrowBalance, err := chainC.GetBalance(ctx, thirdHopEscrowAccount, secondHopIBCDenom) + require.NoError(t, err) + + require.Equal(t, int64(0), firstHopEscrowBalance) + require.Equal(t, int64(0), secondHopEscrowBalance) + require.Equal(t, int64(0), thirdHopEscrowBalance) + }) - // Check that the funds sent ended up in the acc on gaia - 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) + t.Run("multi-hop through native chain ack error refund", func(t *testing.T) { + // send normal IBC transfer from B->A to get funds in IBC denom, then do multihop A->B(native)->C->D + // this lets us test the burn from escrow account on chain C and the escrow to escrow transfer on chain B. + + // Compose the prefixed denoms and ibc denom for asserting balances + baDenom := transfertypes.GetPrefixedDenom(abChan.PortID, abChan.ChannelID, chainB.Config().Denom) + bcDenom := transfertypes.GetPrefixedDenom(cbChan.PortID, cbChan.ChannelID, chainB.Config().Denom) + cdDenom := transfertypes.GetPrefixedDenom(dcChan.PortID, dcChan.ChannelID, bcDenom) + + baDenomTrace := transfertypes.ParseDenomTrace(baDenom) + bcDenomTrace := transfertypes.ParseDenomTrace(bcDenom) + cdDenomTrace := transfertypes.ParseDenomTrace(cdDenom) + + baIBCDenom := baDenomTrace.IBCDenom() + bcIBCDenom := bcDenomTrace.IBCDenom() + cdIBCDenom := cdDenomTrace.IBCDenom() + + transfer := ibc.WalletAmount{ + Address: userA.Bech32Address(chainA.Config().Bech32Prefix), + Denom: chainB.Config().Denom, + Amount: transferAmount, + } + + chainBHeight, err := chainB.Height(ctx) + require.NoError(t, err) + + transferTx, err := chainB.SendIBCTransfer(ctx, baChan.ChannelID, userB.KeyName, transfer, ibc.TransferOptions{}) + require.NoError(t, err) + _, err = testutil.PollForAck(ctx, chainB, chainBHeight, chainBHeight+10, transferTx.Packet) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 1, chainB) + require.NoError(t, err) + + // assert balance for user controlled wallet + chainABalance, err := chainA.GetBalance(ctx, userA.Bech32Address(chainA.Config().Bech32Prefix), baIBCDenom) + require.NoError(t, err) + + baEscrowBalance, err := chainB.GetBalance(ctx, transfertypes.GetEscrowAddress(baChan.PortID, baChan.ChannelID).String(), chainB.Config().Denom) + require.NoError(t, err) + + require.Equal(t, transferAmount, chainABalance) + require.Equal(t, transferAmount, baEscrowBalance) + + // Send a malformed packet with invalid receiver address from Chain A->Chain B->Chain C->Chain D + // This should succeed in the first hop and second hop, then fail to make the third hop. + // Funds should be refunded to Chain B and then to Chain A via acknowledgements with errors. + transfer = ibc.WalletAmount{ + Address: userB.Bech32Address(chainB.Config().Bech32Prefix), + Denom: baIBCDenom, + Amount: transferAmount, + } + + secondHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: "xyz1t8eh66t2w5k67kwurmn5gqhtq6d2ja0vp7jmmq", // malformed receiver address on chain D + Channel: cdChan.ChannelID, + Port: cdChan.PortID, + }, + } + + nextBz, err := json.Marshal(secondHopMetadata) + require.NoError(t, err) + + next := string(nextBz) + + firstHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userC.Bech32Address(chainC.Config().Bech32Prefix), + Channel: bcChan.ChannelID, + Port: bcChan.PortID, + Next: &next, + }, + } + + memo, err := json.Marshal(firstHopMetadata) + require.NoError(t, err) + + chainAHeight, err := chainA.Height(ctx) + require.NoError(t, err) + + transferTx, err = chainA.SendIBCTransfer(ctx, abChan.ChannelID, userA.KeyName, transfer, ibc.TransferOptions{Memo: string(memo)}) + require.NoError(t, err) + _, err = testutil.PollForAck(ctx, chainA, chainAHeight, chainAHeight+30, transferTx.Packet) + require.NoError(t, err) + err = testutil.WaitForBlocks(ctx, 1, chainA) + require.NoError(t, err) + + // assert balances for user controlled wallets + chainDBalance, err := chainD.GetBalance(ctx, userD.Bech32Address(chainD.Config().Bech32Prefix), cdIBCDenom) + require.NoError(t, err) + + chainCBalance, err := chainC.GetBalance(ctx, userC.Bech32Address(chainC.Config().Bech32Prefix), bcIBCDenom) + require.NoError(t, err) + + chainBBalance, err := chainB.GetBalance(ctx, userB.Bech32Address(chainB.Config().Bech32Prefix), chainB.Config().Denom) + require.NoError(t, err) + + chainABalance, err = chainA.GetBalance(ctx, userA.Bech32Address(chainA.Config().Bech32Prefix), baIBCDenom) + require.NoError(t, err) + + require.Equal(t, transferAmount, chainABalance) + require.Equal(t, userFunds-transferAmount, chainBBalance) + require.Equal(t, int64(0), chainCBalance) + require.Equal(t, int64(0), chainDBalance) + + // assert balances for IBC escrow accounts + cdEscrowBalance, err := chainC.GetBalance(ctx, transfertypes.GetEscrowAddress(cdChan.PortID, cdChan.ChannelID).String(), bcIBCDenom) + require.NoError(t, err) + + bcEscrowBalance, err := chainB.GetBalance(ctx, transfertypes.GetEscrowAddress(bcChan.PortID, bcChan.ChannelID).String(), chainB.Config().Denom) + require.NoError(t, err) + + baEscrowBalance, err = chainB.GetBalance(ctx, transfertypes.GetEscrowAddress(baChan.PortID, baChan.ChannelID).String(), chainB.Config().Denom) + require.NoError(t, err) + + require.Equal(t, transferAmount, baEscrowBalance) + require.Equal(t, int64(0), bcEscrowBalance) + require.Equal(t, int64(0), cdEscrowBalance) + }) } diff --git a/ibc/chain.go b/ibc/chain.go index 7a6cbe979..57bb8a015 100644 --- a/ibc/chain.go +++ b/ibc/chain.go @@ -57,7 +57,7 @@ type Chain interface { SendFunds(ctx context.Context, keyName string, amount WalletAmount) error // SendIBCTransfer sends an IBC transfer returning a transaction or an error if the transfer failed. - SendIBCTransfer(ctx context.Context, channelID, keyName string, amount WalletAmount, timeout *IBCTimeout) (Tx, error) + SendIBCTransfer(ctx context.Context, channelID, keyName string, amount WalletAmount, options TransferOptions) (Tx, error) // Height returns the current block height or an error if unable to get current height. Height(ctx context.Context) (uint64, error) @@ -74,3 +74,9 @@ type Chain interface { // Timeouts returns all timeouts in a block at height. Timeouts(ctx context.Context, height uint64) ([]PacketTimeout, error) } + +// TransferOptions defines the options for an IBC packet transfer. +type TransferOptions struct { + Timeout *IBCTimeout + Memo string +} diff --git a/ibc/relayer.go b/ibc/relayer.go index b80302b1d..c8ab0bb98 100644 --- a/ibc/relayer.go +++ b/ibc/relayer.go @@ -53,6 +53,9 @@ type Relayer interface { // GetConnections returns a slice of IBC connection details composed of the details for each connection on a specified chain. GetConnections(ctx context.Context, rep RelayerExecReporter, chainID string) (ConnectionOutputs, error) + // GetClients returns a slice of IBC client details composed of the details for each client on a specified chain. + GetClients(ctx context.Context, rep RelayerExecReporter, chainID string) (ClientOutputs, error) + // After configuration is initialized, begin relaying. // This method is intended to create a background worker that runs the relayer. // You must call StopRelayer to cleanly stop the relaying. @@ -95,6 +98,82 @@ type Relayer interface { Exec(ctx context.Context, rep RelayerExecReporter, cmd []string, env []string) RelayerExecResult } +// GetTransferChannel will return the transfer channel assuming only one client, +// one connection, and one channel with "transfer" port exists between two chains. +func GetTransferChannel(ctx context.Context, r Relayer, rep RelayerExecReporter, srcChainID, dstChainID string) (*ChannelOutput, error) { + srcClients, err := r.GetClients(ctx, rep, srcChainID) + if err != nil { + return nil, fmt.Errorf("failed to get clients on source chain: %w", err) + } + + if len(srcClients) == 0 { + return nil, fmt.Errorf("no clients exist on source chain: %w", err) + } + + var srcClientID string + for _, client := range srcClients { + // TODO continue for expired clients + if client.ClientState.ChainID == dstChainID { + if srcClientID != "" { + return nil, fmt.Errorf("found multiple clients on %s tracking %s", srcChainID, dstChainID) + } + srcClientID = client.ClientID + } + } + + if srcClientID == "" { + return nil, fmt.Errorf("unable to find client on %s tracking %s", srcChainID, dstChainID) + } + + srcConnections, err := r.GetConnections(ctx, rep, srcChainID) + if err != nil { + return nil, fmt.Errorf("failed to get connections on source chain: %w", err) + } + + if len(srcConnections) == 0 { + return nil, fmt.Errorf("no connections exist on source chain: %w", err) + } + + var srcConnectionID string + for _, connection := range srcConnections { + if connection.ClientID == srcClientID { + if srcConnectionID != "" { + return nil, fmt.Errorf("found multiple connections on %s for client %s", srcChainID, srcClientID) + } + srcConnectionID = connection.ID + } + } + + if srcConnectionID == "" { + return nil, fmt.Errorf("unable to find connection on %s for client %s", srcChainID, srcClientID) + } + + srcChannels, err := r.GetChannels(ctx, rep, srcChainID) + if err != nil { + return nil, fmt.Errorf("failed to get channels on source chain: %w", err) + } + + if len(srcChannels) == 0 { + return nil, fmt.Errorf("no channels exist on source chain: %w", err) + } + + var srcChan *ChannelOutput + for _, channel := range srcChannels { + if len(channel.ConnectionHops) == 1 && channel.ConnectionHops[0] == srcConnectionID && channel.PortID == "transfer" { + if srcChan != nil { + return nil, fmt.Errorf("found multiple transfer channels on %s for connection %s", srcChainID, srcConnectionID) + } + srcChan = &channel + } + } + + if srcChan == nil { + return nil, fmt.Errorf("no transfer channel found between chains: %s - %s", srcChainID, dstChainID) + } + + return srcChan, nil +} + // RelyaerExecResult holds the details of a call to Relayer.Exec. type RelayerExecResult struct { // This type is a redeclaration of dockerutil.ContainerExecResult. diff --git a/ibc/types.go b/ibc/types.go index e782d7870..33ed81785 100644 --- a/ibc/types.go +++ b/ibc/types.go @@ -174,6 +174,17 @@ type ConnectionOutput struct { type ConnectionOutputs []*ConnectionOutput +type ClientOutput struct { + ClientID string `json:"client_id"` + ClientState ClientState `json:"client_state"` +} + +type ClientState struct { + ChainID string `json:"chain_id"` +} + +type ClientOutputs []*ClientOutput + type Wallet struct { Mnemonic string `json:"mnemonic"` Address string `json:"address"` diff --git a/interchain_test.go b/interchain_test.go index 517ab9d67..cfa82e116 100644 --- a/interchain_test.go +++ b/interchain_test.go @@ -281,7 +281,16 @@ func TestCosmosChain_BroadcastTx(t *testing.T) { b := cosmos.NewBroadcaster(t, gaia0.(*cosmos.CosmosChain)) transferAmount := types.Coin{Denom: gaia0.Config().Denom, Amount: types.NewInt(sendAmount)} - msg := transfertypes.NewMsgTransfer("transfer", "channel-0", transferAmount, testUser.Bech32Address(gaia0.Config().Bech32Prefix), testUser.Bech32Address(gaia1.Config().Bech32Prefix), clienttypes.NewHeight(1, 1000), 0) + msg := transfertypes.NewMsgTransfer( + "transfer", + "channel-0", + transferAmount, + testUser.Bech32Address(gaia0.Config().Bech32Prefix), + testUser.Bech32Address(gaia1.Config().Bech32Prefix), + clienttypes.NewHeight(1, 1000), + 0, + "", + ) resp, err := cosmos.BroadcastTx(ctx, b, testUser, msg) require.NoError(t, err) assertTransactionIsValid(t, resp) diff --git a/internal/blockdb/messages_view_test.go b/internal/blockdb/messages_view_test.go index b325db855..8d5b1f5b9 100644 --- a/internal/blockdb/messages_view_test.go +++ b/internal/blockdb/messages_view_test.go @@ -255,11 +255,12 @@ WHERE type = "/ibc.core.channel.v1.MsgChannelOpenConfirm" AND chain_id = ? // Send the IBC transfer. Relayer isn't running, so this will just create a MsgTransfer. const txAmount = 13579 // Arbitrary amount that is easy to find in logs. - tx, err := gaia0.SendIBCTransfer(ctx, gaia0ChannelID, ibctest.FaucetAccountKeyName, ibc.WalletAmount{ + transfer := ibc.WalletAmount{ Address: gaia1FaucetAddr, Denom: gaia0.Config().Denom, Amount: txAmount, - }, nil) + } + tx, err := gaia0.SendIBCTransfer(ctx, gaia0ChannelID, ibctest.FaucetAccountKeyName, transfer, ibc.TransferOptions{}) require.NoError(t, err) require.NoError(t, tx.Validate()) diff --git a/relayer/docker.go b/relayer/docker.go index 53d2f878c..e22cfba03 100644 --- a/relayer/docker.go +++ b/relayer/docker.go @@ -282,6 +282,16 @@ func (r *DockerRelayer) GetConnections(ctx context.Context, rep ibc.RelayerExecR return r.c.ParseGetConnectionsOutput(string(res.Stdout), string(res.Stderr)) } +func (r *DockerRelayer) GetClients(ctx context.Context, rep ibc.RelayerExecReporter, chainID string) (ibc.ClientOutputs, error) { + cmd := r.c.GetClients(chainID, r.HomeDir()) + res := r.Exec(ctx, rep, cmd, nil) + if res.Err != nil { + return nil, res.Err + } + + return r.c.ParseGetClientsOutput(string(res.Stdout), string(res.Stderr)) +} + func (r *DockerRelayer) LinkPath(ctx context.Context, rep ibc.RelayerExecReporter, pathName string, channelOpts ibc.CreateChannelOptions, clientOpts ibc.CreateClientOptions) error { cmd := r.c.LinkPath(pathName, r.HomeDir(), channelOpts, clientOpts) res := r.Exec(ctx, rep, cmd, nil) @@ -637,6 +647,10 @@ type RelayerCommander interface { // to produce the connection output values. ParseGetConnectionsOutput(stdout, stderr string) (ibc.ConnectionOutputs, error) + // ParseGetClientsOutput processes the output of GetClients + // to produce the client output values. + ParseGetClientsOutput(stdout, stderr string) (ibc.ClientOutputs, error) + // Init is the command to run on the first call to AddChainConfiguration. // If the returned command is nil or empty, nothing will be executed. Init(homeDir string) []string @@ -654,6 +668,7 @@ type RelayerCommander interface { UpdatePath(pathName, homeDir string, filter ibc.ChannelFilter) []string GetChannels(chainID, homeDir string) []string GetConnections(chainID, homeDir string) []string + GetClients(chainID, homeDir string) []string LinkPath(pathName, homeDir string, channelOpts ibc.CreateChannelOptions, clientOpts ibc.CreateClientOptions) []string RestoreKey(chainID, keyName, mnemonic, homeDir string) []string StartRelayer(homeDir string, pathNames ...string) []string diff --git a/relayer/rly/cosmos_relayer.go b/relayer/rly/cosmos_relayer.go index 1d0ca4f1a..0a64c9581 100644 --- a/relayer/rly/cosmos_relayer.go +++ b/relayer/rly/cosmos_relayer.go @@ -201,6 +201,13 @@ func (commander) GetConnections(chainID, homeDir string) []string { } } +func (commander) GetClients(chainID, homeDir string) []string { + return []string{ + "rly", "q", "clients", chainID, + "--home", homeDir, + } +} + func (commander) LinkPath(pathName, homeDir string, channelOpts ibc.CreateChannelOptions, clientOpt ibc.CreateClientOptions) []string { return []string{ "rly", "tx", "link", pathName, @@ -306,6 +313,28 @@ func (c commander) ParseGetConnectionsOutput(stdout, stderr string) (ibc.Connect return connections, nil } +func (c commander) ParseGetClientsOutput(stdout, stderr string) (ibc.ClientOutputs, error) { + var clients ibc.ClientOutputs + for _, client := range strings.Split(stdout, "\n") { + if strings.TrimSpace(client) == "" { + continue + } + + var clientOutput ibc.ClientOutput + if err := json.Unmarshal([]byte(client), &clientOutput); err != nil { + c.log.Error( + "Error parsing client json", + zap.Error(err), + ) + + continue + } + clients = append(clients, &clientOutput) + } + + return clients, nil +} + func (commander) Init(homeDir string) []string { return []string{ "rly", "config", "init",