diff --git a/.github/workflows/e2e-test-workflow-call.yml b/.github/workflows/e2e-test-workflow-call.yml index 7d8b2e643cb..5ba77a49e9f 100644 --- a/.github/workflows/e2e-test-workflow-call.yml +++ b/.github/workflows/e2e-test-workflow-call.yml @@ -36,6 +36,16 @@ on: description: 'The tag to use for chain B' required: true type: string + chain-c-tag: + description: 'The tag to use for chain C' + required: true + type: string + default: main + chain-d-tag: + default: main + description: 'The tag to use for chain D' + required: true + type: string # upgrade-plan-name is only required during upgrade tests, and is otherwise ignored. upgrade-plan-name: default: '' @@ -78,6 +88,8 @@ jobs: echo "Chain Image: ${{ inputs.chain-image }}" echo "Chain A Tag: ${{ inputs.chain-a-tag }}" echo "Chain B Tag: ${{ inputs.chain-b-tag }}" + echo "Chain C Tag: ${{ inputs.chain-c-tag }}" + echo "Chain D Tag: ${{ inputs.chain-d-tag }}" echo "Upgrade Plan Name: ${{ inputs.upgrade-plan-name }}" echo "Test Entry Point: ${{ inputs.test-entry-point }}" echo "Test: ${{ inputs.test }}" @@ -205,6 +217,8 @@ jobs: CHAIN_UPGRADE_PLAN: '${{ inputs.upgrade-plan-name }}' CHAIN_A_TAG: '${{ inputs.chain-a-tag }}' CHAIN_B_TAG: '${{ inputs.chain-b-tag }}' + CHAIN_C_TAG: '${{ inputs.chain-c-tag }}' + CHAIN_D_TAG: '${{ inputs.chain-d-tag }}' E2E_CONFIG_PATH: '${{ inputs.e2e-config-path }}' strategy: fail-fast: false @@ -243,6 +257,8 @@ jobs: CHAIN_IMAGE: '${{ inputs.chain-image }}' CHAIN_A_TAG: '${{ inputs.chain-a-tag }}' CHAIN_B_TAG: '${{ inputs.chain-b-tag }}' + CHAIN_C_TAG: '${{ inputs.chain-c-tag }}' + CHAIN_D_TAG: '${{ inputs.chain-d-tag }}' E2E_CONFIG_PATH: '${{ inputs.e2e-config-path }}' strategy: fail-fast: false @@ -256,6 +272,7 @@ jobs: - entrypoint: TestTransferLocalhostTestSuite - entrypoint: TestConnectionTestSuite - entrypoint: TestInterchainAccountsGovTestSuite + - entrypoint: TestForwardTransferSuite steps: - uses: actions/checkout@v4 with: diff --git a/.github/workflows/e2e-upgrade.yaml b/.github/workflows/e2e-upgrade.yaml index 610362e1b86..1b06a279eae 100644 --- a/.github/workflows/e2e-upgrade.yaml +++ b/.github/workflows/e2e-upgrade.yaml @@ -68,6 +68,8 @@ jobs: CHAIN_IMAGE: '${{ env.DOCKER_IMAGE_NAME }}' CHAIN_A_TAG: '${{ matrix.test-config.tag }}' CHAIN_B_TAG: '${{ matrix.test-config.tag }}' + CHAIN_C_TAG: '${{ matrix.test-config.tag }}' + CHAIN_D_TAG: '${{ matrix.test-config.tag }}' CHAIN_UPGRADE_PLAN: '${{ matrix.test-config.upgrade-plan }}' E2E_CONFIG_PATH: 'ci-e2e-config.yaml' run: | diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 5362c431b76..673ff7e4147 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -104,6 +104,8 @@ jobs: CHAIN_IMAGE: '${{ env.DOCKER_IMAGE_NAME }}' CHAIN_A_TAG: '${{ needs.determine-image-tag.outputs.simd-tag }}' CHAIN_B_TAG: '${{ needs.determine-image-tag.outputs.simd-tag }}' + CHAIN_C_TAG: '${{ needs.determine-image-tag.outputs.simd-tag }}' + CHAIN_D_TAG: '${{ needs.determine-image-tag.outputs.simd-tag }}' E2E_CONFIG_PATH: 'ci-e2e-config.yaml' run: | cd e2e diff --git a/CHANGELOG.md b/CHANGELOG.md index cc70e396549..e4a6e495053 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,8 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features +* [\#8285](https://github.com/cosmos/ibc-go/pull/8285) Packet forward middleware. + ### Dependencies * [\#8369](https://github.com/cosmos/ibc-go/pull/8369) Bump **github.com/CosmWasm/wasmvm** to **2.2.4** diff --git a/docs/docs/02-apps/03-packet-forward-middleware/integration.md b/docs/docs/02-apps/03-packet-forward-middleware/integration.md new file mode 100644 index 00000000000..f9e1b6940be --- /dev/null +++ b/docs/docs/02-apps/03-packet-forward-middleware/integration.md @@ -0,0 +1,172 @@ +# Integration + +This document provides instructions on integrating and configuring the Packet Forward Middleware (PFM) within your +existing chain implementation. This document is *NOT* a guide on developing with the Cosmos SDK or ibc-go and makes +the assumption that you have some existing codebase for your chain with IBC already enabled. + +The integration steps include the following: + +1. Import the PFM, initialize the PFM Module & Keeper, initialize the store keys and module params, and initialize the Begin/End Block logic and InitGenesis order. +2. Configure the IBC application stack including the transfer module. +3. Configuration of additional options such as timeout period, number of retries on timeout, refund timeout period, and fee percentage. + +Integration of the PFM should take approximately 20 minutes. + +## Example integration of the Packet Forward Middleware + +```go +// app.go + +// Import the packet forward middleware +import ( + "github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v10/packetforward" + packetforwardkeeper "github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v10/packetforward/keeper" + packetforwardtypes "github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v10/packetforward/types" +) + +... + +// Register the AppModule for the packet forward middleware module +ModuleBasics = module.NewBasicManager( + ... + packetforward.AppModuleBasic{}, + ... +) + +... + +// Add packet forward middleware Keeper +type App struct { + ... + PacketForwardKeeper *packetforwardkeeper.Keeper + ... +} + +... + +// Create store keys +keys := sdk.NewKVStoreKeys( + ... + packetforwardtypes.StoreKey, + ... +) + +... + +// Initialize the packet forward middleware Keeper +// It's important to note that the PFM Keeper must be initialized before the Transfer Keeper +app.PacketForwardKeeper = packetforwardkeeper.NewKeeper( + appCodec, + keys[packetforwardtypes.StoreKey], + nil, // will be zero-value here, reference is set later on with SetTransferKeeper. + app.IBCKeeper.ChannelKeeper, + appKeepers.DistrKeeper, + app.BankKeeper, + app.IBCKeeper.ChannelKeeper, + authtypes.NewModuleAddress(govtypes.ModuleName).String(), +) + +// Initialize the transfer module Keeper +app.TransferKeeper = ibctransferkeeper.NewKeeper( + appCodec, + keys[ibctransfertypes.StoreKey], + app.GetSubspace(ibctransfertypes.ModuleName), + app.PacketForwardKeeper, + app.IBCKeeper.ChannelKeeper, + &app.IBCKeeper.PortKeeper, + app.AccountKeeper, + app.BankKeeper, + scopedTransferKeeper, +) + +app.PacketForwardKeeper.SetTransferKeeper(app.TransferKeeper) + +// See the section below for configuring an application stack with the packet forward middleware + +... + +// Register packet forward middleware AppModule +app.moduleManager = module.NewManager( + ... + packetforward.NewAppModule(app.PacketForwardKeeper, app.GetSubspace(packetforwardtypes.ModuleName)), +) + +... + +// Add packet forward middleware to begin blocker logic +app.moduleManager.SetOrderBeginBlockers( + ... + packetforwardtypes.ModuleName, + ... +) + +// Add packet forward middleware to end blocker logic +app.moduleManager.SetOrderEndBlockers( + ... + packetforwardtypes.ModuleName, + ... +) + +// Add packet forward middleware to init genesis logic +app.moduleManager.SetOrderInitGenesis( + ... + packetforwardtypes.ModuleName, + ... +) + +// Add packet forward middleware to init params keeper +func initParamsKeeper(appCodec codec.BinaryCodec, legacyAmino *codec.LegacyAmino, key, tkey storetypes.StoreKey) paramskeeper.Keeper { + ... + paramsKeeper.Subspace(packetforwardtypes.ModuleName).WithKeyTable(packetforwardtypes.ParamKeyTable()) + ... +} +``` + +## Configuring the transfer application stack with Packet Forward Middleware + +Here is an example of how to create an application stack using `transfer` and `packet-forward-middleware`. +The following `transferStack` is configured in `app/app.go` and added to the IBC `Router`. +The in-line comments describe the execution flow of packets between the application stack and IBC core. + +For more information on configuring an IBC application stack see the ibc-go docs [here](https://github.com/cosmos/ibc-go/blob/e69a833de764fa0f5bdf0338d9452fd6e579a675/docs/docs/04-middleware/01-ics29-fee/02-integration.md#configuring-an-application-stack-with-fee-middleware). + +```go +// Create Transfer Stack +// SendPacket, since it is originating from the application to core IBC: +// transferKeeper.SendPacket -> packetforward.SendPacket -> channel.SendPacket + +// RecvPacket, message that originates from core IBC and goes down to app, the flow is the other way +// channel.RecvPacket -> packetforward.OnRecvPacket -> transfer.OnRecvPacket + +// transfer stack contains (from top to bottom): +// - Packet Forward Middleware +// - Transfer +var transferStack ibcporttypes.IBCModule +transferStack = transfer.NewIBCModule(app.TransferKeeper) +transferStack = packetforward.NewIBCMiddleware( + transferStack, + app.PacketForwardKeeper, + 0, // retries on timeout + packetforwardkeeper.DefaultForwardTransferPacketTimeoutTimestamp, // forward timeout +) + +// Add transfer stack to IBC Router +ibcRouter.AddRoute(ibctransfertypes.ModuleName, transferStack) +``` + +## Configurable options in the Packet Forward Middleware + +The Packet Forward Middleware has several configurable options available when initializing the IBC application stack. +You can see these passed in as arguments to `packetforward.NewIBCMiddleware` and they include the number of retries that +will be performed on a forward timeout, the timeout period that will be used for a forward, and the timeout period that +will be used for performing refunds in the case that a forward is taking too long. + +Additionally, there is a fee percentage parameter that can be set in `InitGenesis`, this is an optional parameter that +can be used to take a fee from each forwarded packet which will then be distributed to the community pool. In the +`OnRecvPacket` callback `ForwardTransferPacket` is invoked which will attempt to subtract a fee from the forwarded +packet amount if the fee percentage is non-zero. + +- Retries On Timeout - how many times will a forward be re-attempted in the case of a timeout. +- Timeout Period - how long can a forward be in progress before giving up. +- Refund Timeout - how long can a forward be in progress before issuing a refund back to the original source chain. +- Fee Percentage - % of the forwarded packet amount which will be subtracted and distributed to the community pool. diff --git a/e2e/go.mod b/e2e/go.mod index b019545a9ac..aca86de1415 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -152,6 +152,7 @@ require ( github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/holiman/uint256 v1.3.2 // indirect github.com/huandu/skiplist v1.2.1 // indirect + github.com/iancoleman/orderedmap v0.3.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/icza/dyno v0.0.0-20230330125955-09f820a8d9c0 // indirect github.com/improbable-eng/grpc-web v0.15.0 // indirect diff --git a/e2e/go.sum b/e2e/go.sum index 1cd92728c91..8f338682313 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -1213,6 +1213,8 @@ github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0Jr github.com/huandu/skiplist v1.2.1 h1:dTi93MgjwErA/8idWTzIw4Y1kZsMWx35fmI2c8Rij7w= github.com/huandu/skiplist v1.2.1/go.mod h1:7v3iFjLcSAzO4fN5B8dvebvo/qsfumiLiDXMrPiHF9w= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= diff --git a/e2e/sample.config.extended.yaml b/e2e/sample.config.extended.yaml index 8ffe75e705e..0539caf97b0 100644 --- a/e2e/sample.config.extended.yaml +++ b/e2e/sample.config.extended.yaml @@ -7,6 +7,8 @@ # | CHAIN_IMAGE | The image that will be used for the chain | ghcr.io/cosmos/ibc-go-simd | # | CHAIN_A_TAG | The tag used for chain A | N/A (must be set) | # | CHAIN_B_TAG | The tag used for chain B | N/A (must be set) | +# | CHAIN_C_TAG | The tag used for chain C | Optional (fallback to A) | +# | CHAIN_D_TAG | The tag used for chain D | Optional (fallback to A) | # | CHAIN_BINARY | The binary used in the container | simd | # | RELAYER_TAG | The tag used for the relayer | 1.10.4 | # | RELAYER_ID | The type of relayer to use (rly/hermes) | hermes | @@ -23,13 +25,29 @@ chains: tag: main # override with CHAIN_A_TAG binary: simd # override with CHAIN_BINARY - # the entry at index 1 corresponds to CHAIN_B + # the entry at index 1 corresponds to CHAIN_B - chainId: chainB-1 numValidators: 4 numFullNodes: 1 image: ghcr.io/cosmos/ibc-go-simd # override with CHAIN_IMAGE tag: main # override with CHAIN_B_TAG binary: simd # override with CHAIN_BINARY + + # the entry at index 2 corresponds to CHAIN_C + - chainId: chainC-1 + numValidators: 4 + numFullNodes: 1 + image: ghcr.io/cosmos/ibc-go-simd # override with CHAIN_IMAGE + tag: main # override with CHAIN_C_TAG + binary: simd # override with CHAIN_BINARY + + # the entry at index 3 corresponds to CHAIN_D + - chainId: chainD-1 + numValidators: 4 + numFullNodes: 1 + image: ghcr.io/cosmos/ibc-go-simd # override with CHAIN_IMAGE + tag: main # override with CHAIN_D_TAG + binary: simd # override with CHAIN_BINARY # activeRelayer must match the id of a relayer specified in the relayers list below. activeRelayer: hermes # override with RELAYER_ID diff --git a/e2e/sample.config.yaml b/e2e/sample.config.yaml index 2715858f832..a6ab2bdae3e 100644 --- a/e2e/sample.config.yaml +++ b/e2e/sample.config.yaml @@ -6,3 +6,7 @@ chains: chainId: chainA-1 - tag: main # override with CHAIN_B_TAG chainId: chainB-1 + - tag: main # override with CHAIN_C_TAG + chainId: chainC-1 + - tag: main # override with CHAIN_D_TAG + chainId: chainD-1 diff --git a/e2e/tests/core/03-connection/connection_test.go b/e2e/tests/core/03-connection/connection_test.go index 1faac487253..350062a3452 100644 --- a/e2e/tests/core/03-connection/connection_test.go +++ b/e2e/tests/core/03-connection/connection_test.go @@ -40,7 +40,7 @@ func (s *ConnectionTestSuite) SetupSuite() { } func (s *ConnectionTestSuite) CreateConnectionTestPath(testName string) (ibc.Relayer, ibc.ChannelOutput) { - return s.CreatePaths(ibc.DefaultClientOpts(), s.TransferChannelOptions(), testName), s.GetChainAChannelForTest(testName) + return s.CreatePaths(ibc.DefaultClientOpts(), s.TransferChannelOptions(), testName), s.GetChainAToChainBChannel(testName) } // QueryMaxExpectedTimePerBlockParam queries the on-chain max expected time per block param for 03-connection diff --git a/e2e/tests/packet_forward_middleware/forward_timeout_test.go b/e2e/tests/packet_forward_middleware/forward_timeout_test.go new file mode 100644 index 00000000000..5923709ab14 --- /dev/null +++ b/e2e/tests/packet_forward_middleware/forward_timeout_test.go @@ -0,0 +1,484 @@ +//go:build !test_e2e + +package pfm + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + "github.com/strangelove-ventures/interchaintest/v8/testreporter" + "github.com/strangelove-ventures/interchaintest/v8/testutil" + + "cosmossdk.io/math" + + "github.com/cosmos/ibc-go/e2e/testsuite" + "github.com/cosmos/ibc-go/e2e/testvalues" + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + chantypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" +) + +type PFMTimeoutTestSuite struct { + testsuite.E2ETestSuite +} + +func TestForwardTransferTimeoutSuite(t *testing.T) { + // TODO: Enable as we clean up these tests #8360 + t.Skip("Skipping as relayer is not relaying failed packets") + // testifysuite.Run(t, new(PFMTimeoutTestSuite)) +} + +func (s *PFMTimeoutTestSuite) TestTimeoutOnForward() { + t := s.T() + t.Parallel() + + ctx := context.TODO() + + chains := s.GetAllChains() + a, b, c, d := chains[0], chains[1], chains[2], chains[3] + + relayer := s.CreatePaths(ibc.DefaultClientOpts(), s.TransferChannelOptions(), t.Name()) + s.StartRelayer(relayer, t.Name()) + + // Fund user accounts with initial balances and get the transfer channel information between each set of chains + usrA := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + usrB := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + usrC := s.CreateUserOnChainC(ctx, testvalues.StartingTokenAmount) + usrD := s.CreateUserOnChainD(ctx, testvalues.StartingTokenAmount) + + abChan := s.GetChainAToChainBChannel(t.Name()) + baChan := abChan.Counterparty + bcChan := s.GetChainBToChainCChannel(t.Name()) + cbChan := bcChan.Counterparty + cdChan := s.GetChainCToChainDChannel(t.Name()) + dcChan := cdChan.Counterparty + + retries := uint8(0) + secondHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: usrD.FormattedAddress(), + Channel: cdChan.ChannelID, + Port: cdChan.PortID, + Retries: &retries, + }, + } + + nextBz, err := json.Marshal(secondHopMetadata) + s.Require().NoError(err) + next := string(nextBz) + + firstHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: usrC.FormattedAddress(), + Channel: bcChan.ChannelID, + Port: bcChan.PortID, + Next: &next, + Retries: &retries, + Timeout: time.Second * 10, // Set low timeout for forward from chainB<>chainC + }, + } + + memo, err := json.Marshal(firstHopMetadata) + s.Require().NoError(err) + + opts := ibc.TransferOptions{ + Memo: string(memo), + } + + bHeight, err := b.Height(ctx) + s.Require().NoError(err) + + transferAmount := math.NewInt(100_000) + // Attempt to send packet from a -> b -> c -> d + amount := ibc.WalletAmount{ + Address: usrB.FormattedAddress(), + Denom: a.Config().Denom, + Amount: transferAmount, + } + + transferTx, err := a.SendIBCTransfer(ctx, abChan.ChannelID, usrA.KeyName(), amount, opts) + s.Require().NoError(err) + + // Poll for MsgRecvPacket on chainB + _, err = cosmos.PollForMessage[*chantypes.MsgRecvPacket](ctx, b.(*cosmos.CosmosChain), cosmos.DefaultEncoding().InterfaceRegistry, bHeight, bHeight+20, nil) + s.Require().NoError(err) + + // Stop the relayer and wait for the timeout to happen on chainC + rep := testreporter.NewNopReporter() + eRep := rep.RelayerExecReporter(t) + err = relayer.StopRelayer(ctx, eRep) + s.Require().NoError(err) + + time.Sleep(time.Second * 11) + s.StartRelayer(relayer, t.Name()) + + aHeight, err := a.Height(ctx) + s.Require().NoError(err) + + bHeight, err = b.Height(ctx) + s.Require().NoError(err) + + // Poll for the MsgTimeout on chainB and the MsgAck on chainA + _, err = cosmos.PollForMessage[*chantypes.MsgTimeout](ctx, b.(*cosmos.CosmosChain), b.Config().EncodingConfig.InterfaceRegistry, bHeight, bHeight+30, nil) + s.Require().NoError(err) + + _, err = testutil.PollForAck(ctx, a, aHeight, aHeight+30, transferTx.Packet) + s.Require().NoError(err) + + err = testutil.WaitForBlocks(ctx, 1, a) + s.Require().NoError(err) + + // Assert balances to ensure that the funds are still on the original sending chain + chainABalance, err := a.GetBalance(ctx, usrA.FormattedAddress(), a.Config().Denom) + s.Require().NoError(err) + + // Compose the prefixed denoms and ibc denom for asserting balances + firstHopDenom := transfertypes.GetPrefixedDenom(baChan.PortID, baChan.ChannelID, a.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() + + chainBBalance, err := b.GetBalance(ctx, usrB.FormattedAddress(), firstHopIBCDenom) + s.Require().NoError(err) + + chainCBalance, err := c.GetBalance(ctx, usrC.FormattedAddress(), secondHopIBCDenom) + s.Require().NoError(err) + + chainDBalance, err := d.GetBalance(ctx, usrD.FormattedAddress(), thirdHopIBCDenom) + s.Require().NoError(err) + + initBal := math.NewInt(10_000_000_000) + zeroBal := math.NewInt(0) + + s.Require().True(chainCBalance.Equal(zeroBal)) + s.Require().True(chainBBalance.Equal(zeroBal)) + s.Require().True(chainABalance.Equal(initBal)) + s.Require().True(chainDBalance.Equal(zeroBal)) + + firstHopEscrowAccount := transfertypes.GetEscrowAddress(abChan.PortID, abChan.ChannelID).String() + secondHopEscrowAccount := transfertypes.GetEscrowAddress(bcChan.PortID, bcChan.ChannelID).String() + thirdHopEscrowAccount := transfertypes.GetEscrowAddress(cdChan.PortID, abChan.ChannelID).String() + + firstHopEscrowBalance, err := a.GetBalance(ctx, firstHopEscrowAccount, a.Config().Denom) + s.Require().NoError(err) + + secondHopEscrowBalance, err := b.GetBalance(ctx, secondHopEscrowAccount, firstHopIBCDenom) + s.Require().NoError(err) + + thirdHopEscrowBalance, err := c.GetBalance(ctx, thirdHopEscrowAccount, secondHopIBCDenom) + s.Require().NoError(err) + + s.Require().True(firstHopEscrowBalance.Equal(zeroBal)) + s.Require().True(secondHopEscrowBalance.Equal(zeroBal)) + s.Require().True(thirdHopEscrowBalance.Equal(zeroBal)) + + // Send IBC transfer from ChainA -> ChainB -> ChainC -> ChainD that will succeed + secondHopMetadata = &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: usrD.FormattedAddress(), + Channel: cdChan.ChannelID, + Port: cdChan.PortID, + }, + } + nextBz, err = json.Marshal(secondHopMetadata) + s.Require().NoError(err) + next = string(nextBz) + + firstHopMetadata = &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: usrC.FormattedAddress(), + Channel: bcChan.ChannelID, + Port: bcChan.PortID, + Next: &next, + }, + } + + memo, err = json.Marshal(firstHopMetadata) + s.Require().NoError(err) + + opts = ibc.TransferOptions{ + Memo: string(memo), + } + + aHeight, err = a.Height(ctx) + s.Require().NoError(err) + + transferTx, err = a.SendIBCTransfer(ctx, abChan.ChannelID, usrA.KeyName(), amount, opts) + s.Require().NoError(err) + + _, err = testutil.PollForAck(ctx, a, aHeight, aHeight+30, transferTx.Packet) + s.Require().NoError(err) + + err = testutil.WaitForBlocks(ctx, 10, a) + s.Require().NoError(err) + + // Assert balances are updated to reflect tokens now being on ChainD + chainABalance, err = a.GetBalance(ctx, usrA.FormattedAddress(), a.Config().Denom) + s.Require().NoError(err) + + chainBBalance, err = b.GetBalance(ctx, usrB.FormattedAddress(), firstHopIBCDenom) + s.Require().NoError(err) + + chainCBalance, err = c.GetBalance(ctx, usrC.FormattedAddress(), secondHopIBCDenom) + s.Require().NoError(err) + + chainDBalance, err = d.GetBalance(ctx, usrD.FormattedAddress(), thirdHopIBCDenom) + s.Require().NoError(err) + + s.Require().True(chainABalance.Equal(initBal.Sub(transferAmount))) + s.Require().True(chainBBalance.Equal(zeroBal)) + s.Require().True(chainCBalance.Equal(zeroBal)) + s.Require().True(chainDBalance.Equal(transferAmount)) + + firstHopEscrowBalance, err = a.GetBalance(ctx, firstHopEscrowAccount, a.Config().Denom) + s.Require().NoError(err) + + secondHopEscrowBalance, err = b.GetBalance(ctx, secondHopEscrowAccount, firstHopIBCDenom) + s.Require().NoError(err) + + thirdHopEscrowBalance, err = c.GetBalance(ctx, thirdHopEscrowAccount, secondHopIBCDenom) + s.Require().NoError(err) + + s.Require().True(firstHopEscrowBalance.Equal(transferAmount)) + s.Require().True(secondHopEscrowBalance.Equal(transferAmount)) + s.Require().True(thirdHopEscrowBalance.Equal(transferAmount)) + + // Compose IBC tx that will attempt to go from ChainD -> ChainC -> ChainB -> ChainA but timeout between ChainB->ChainA + amount = ibc.WalletAmount{ + Address: usrC.FormattedAddress(), + Denom: thirdHopDenom, + Amount: transferAmount, + } + + secondHopMetadata = &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: usrA.FormattedAddress(), + Channel: baChan.ChannelID, + Port: baChan.PortID, + Timeout: 1 * time.Second, + }, + } + + nextBz, err = json.Marshal(secondHopMetadata) + s.Require().NoError(err) + next = string(nextBz) + + firstHopMetadata = &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: usrB.FormattedAddress(), + Channel: cbChan.ChannelID, + Port: cbChan.PortID, + Next: &next, + }, + } + + memo, err = json.Marshal(firstHopMetadata) + s.Require().NoError(err) + + chainDHeight, err := d.Height(ctx) + s.Require().NoError(err) + + transferTx, err = d.SendIBCTransfer(ctx, dcChan.ChannelID, usrD.KeyName(), amount, ibc.TransferOptions{Memo: string(memo)}) + s.Require().NoError(err) + + _, err = testutil.PollForAck(ctx, d, chainDHeight, chainDHeight+25, transferTx.Packet) + s.Require().NoError(err) + + err = testutil.WaitForBlocks(ctx, 5, d) + s.Require().NoError(err) + + // Assert balances to ensure timeout happened and user funds are still present on ChainD + chainABalance, err = a.GetBalance(ctx, usrA.FormattedAddress(), a.Config().Denom) + s.Require().NoError(err) + + chainBBalance, err = b.GetBalance(ctx, usrB.FormattedAddress(), firstHopIBCDenom) + s.Require().NoError(err) + + chainCBalance, err = c.GetBalance(ctx, usrC.FormattedAddress(), secondHopIBCDenom) + s.Require().NoError(err) + + chainDBalance, err = d.GetBalance(ctx, usrD.FormattedAddress(), thirdHopIBCDenom) + s.Require().NoError(err) + + s.Require().True(chainABalance.Equal(initBal.Sub(transferAmount))) + s.Require().True(chainBBalance.Equal(zeroBal)) + s.Require().True(chainCBalance.Equal(zeroBal)) + s.Require().True(chainDBalance.Equal(transferAmount)) + + firstHopEscrowBalance, err = a.GetBalance(ctx, firstHopEscrowAccount, a.Config().Denom) + s.Require().NoError(err) + + secondHopEscrowBalance, err = b.GetBalance(ctx, secondHopEscrowAccount, firstHopIBCDenom) + s.Require().NoError(err) + + thirdHopEscrowBalance, err = c.GetBalance(ctx, thirdHopEscrowAccount, secondHopIBCDenom) + s.Require().NoError(err) + + s.Require().True(firstHopEscrowBalance.Equal(transferAmount)) + s.Require().True(secondHopEscrowBalance.Equal(transferAmount)) + s.Require().True(thirdHopEscrowBalance.Equal(transferAmount)) + + // --- + + // Compose IBC tx that will go from ChainD -> ChainC -> ChainB -> ChainA and succeed. + amount = ibc.WalletAmount{ + Address: usrC.FormattedAddress(), + Denom: thirdHopDenom, + Amount: transferAmount, + } + + secondHopMetadata = &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: usrA.FormattedAddress(), + Channel: baChan.ChannelID, + Port: baChan.PortID, + }, + } + nextBz, err = json.Marshal(secondHopMetadata) + s.Require().NoError(err) + next = string(nextBz) + + firstHopMetadata = &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: usrB.FormattedAddress(), + Channel: cbChan.ChannelID, + Port: cbChan.PortID, + Next: &next, + }, + } + + memo, err = json.Marshal(firstHopMetadata) + s.Require().NoError(err) + + chainDHeight, err = d.Height(ctx) + s.Require().NoError(err) + + transferTx, err = d.SendIBCTransfer(ctx, dcChan.ChannelID, usrD.KeyName(), amount, ibc.TransferOptions{Memo: string(memo)}) + s.Require().NoError(err) + + _, err = testutil.PollForAck(ctx, d, chainDHeight, chainDHeight+25, transferTx.Packet) + s.Require().NoError(err) + + err = testutil.WaitForBlocks(ctx, 5, d) + s.Require().NoError(err) + + // Assert balances to ensure timeout happened and user funds are still present on ChainD + chainABalance, err = a.GetBalance(ctx, usrA.FormattedAddress(), a.Config().Denom) + s.Require().NoError(err) + + chainBBalance, err = b.GetBalance(ctx, usrB.FormattedAddress(), firstHopIBCDenom) + s.Require().NoError(err) + + chainCBalance, err = c.GetBalance(ctx, usrC.FormattedAddress(), secondHopIBCDenom) + s.Require().NoError(err) + + chainDBalance, err = d.GetBalance(ctx, usrD.FormattedAddress(), thirdHopIBCDenom) + s.Require().NoError(err) + + s.Require().True(chainABalance.Equal(initBal)) + s.Require().True(chainBBalance.Equal(zeroBal)) + s.Require().True(chainCBalance.Equal(zeroBal)) + s.Require().True(chainDBalance.Equal(zeroBal)) + + firstHopEscrowBalance, err = a.GetBalance(ctx, firstHopEscrowAccount, a.Config().Denom) + s.Require().NoError(err) + + secondHopEscrowBalance, err = b.GetBalance(ctx, secondHopEscrowAccount, firstHopIBCDenom) + s.Require().NoError(err) + + thirdHopEscrowBalance, err = c.GetBalance(ctx, thirdHopEscrowAccount, secondHopIBCDenom) + s.Require().NoError(err) + + s.Require().True(firstHopEscrowBalance.Equal(zeroBal)) + s.Require().True(secondHopEscrowBalance.Equal(zeroBal)) + s.Require().True(thirdHopEscrowBalance.Equal(zeroBal)) + + // ----- 2 + + // Compose IBC tx that will go from ChainD -> ChainC -> ChainB -> ChainA and succeed. + amount = ibc.WalletAmount{ + Address: usrB.FormattedAddress(), + Denom: a.Config().Denom, + Amount: transferAmount, + } + + firstHopMetadata = &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: usrA.FormattedAddress(), + Channel: baChan.ChannelID, + Port: baChan.PortID, + Timeout: 1 * time.Second, + }, + } + + memo, err = json.Marshal(firstHopMetadata) + s.Require().NoError(err) + + aHeight, err = a.Height(ctx) + s.Require().NoError(err) + + transferTx, err = a.SendIBCTransfer(ctx, abChan.ChannelID, usrA.KeyName(), amount, ibc.TransferOptions{Memo: string(memo)}) + s.Require().NoError(err) + + _, err = testutil.PollForAck(ctx, a, aHeight, aHeight+25, transferTx.Packet) + s.Require().NoError(err) + + err = testutil.WaitForBlocks(ctx, 5, a) + s.Require().NoError(err) + + // Assert balances to ensure timeout happened and user funds are still present on ChainD + chainABalance, err = a.GetBalance(ctx, usrA.FormattedAddress(), a.Config().Denom) + s.Require().NoError(err) + + chainBBalance, err = b.GetBalance(ctx, usrB.FormattedAddress(), firstHopIBCDenom) + s.Require().NoError(err) + + chainCBalance, err = c.GetBalance(ctx, usrC.FormattedAddress(), secondHopIBCDenom) + s.Require().NoError(err) + + chainDBalance, err = d.GetBalance(ctx, usrD.FormattedAddress(), thirdHopIBCDenom) + s.Require().NoError(err) + + s.Require().True(chainABalance.Equal(initBal)) + s.Require().True(chainBBalance.Equal(zeroBal)) + s.Require().True(chainCBalance.Equal(zeroBal)) + s.Require().True(chainDBalance.Equal(zeroBal)) + + firstHopEscrowBalance, err = a.GetBalance(ctx, firstHopEscrowAccount, a.Config().Denom) + s.Require().NoError(err) + + secondHopEscrowBalance, err = b.GetBalance(ctx, secondHopEscrowAccount, firstHopIBCDenom) + s.Require().NoError(err) + + thirdHopEscrowBalance, err = c.GetBalance(ctx, thirdHopEscrowAccount, secondHopIBCDenom) + s.Require().NoError(err) + + s.Require().True(firstHopEscrowBalance.Equal(zeroBal)) + s.Require().True(secondHopEscrowBalance.Equal(zeroBal)) + s.Require().True(thirdHopEscrowBalance.Equal(zeroBal)) +} + +// TODO: Try to replace this with PFM's own version of this struct #8360 +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"` +} diff --git a/e2e/tests/packet_forward_middleware/packet_forward_test.go b/e2e/tests/packet_forward_middleware/packet_forward_test.go new file mode 100644 index 00000000000..aea918688af --- /dev/null +++ b/e2e/tests/packet_forward_middleware/packet_forward_test.go @@ -0,0 +1,339 @@ +//go:build !test_e2e + +package pfm + +import ( + "context" + "encoding/json" + "strings" + "testing" + "time" + + "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + testifysuite "github.com/stretchr/testify/suite" + + "github.com/cosmos/ibc-go/e2e/testsuite" + "github.com/cosmos/ibc-go/e2e/testsuite/query" + "github.com/cosmos/ibc-go/e2e/testvalues" + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + chantypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" + ibctesting "github.com/cosmos/ibc-go/v10/testing" +) + +type PFMTestSuite struct { + testsuite.E2ETestSuite +} + +func TestForwardTransferSuite(t *testing.T) { + testifysuite.Run(t, new(PFMTestSuite)) +} + +func (s *PFMTestSuite) SetupSuite() { + s.SetupChains(context.TODO(), 4, nil) +} + +func (s *PFMTestSuite) TestForwardPacket() { + t := s.T() + ctx := context.TODO() + testName := t.Name() + + chains := s.GetAllChains() + chainA, chainB, chainC, chainD := chains[0], chains[1], chains[2], chains[3] + + userA := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + userB := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + userC := s.CreateUserOnChainC(ctx, testvalues.StartingTokenAmount) + userD := s.CreateUserOnChainD(ctx, testvalues.StartingTokenAmount) + + relayer := s.CreatePaths(ibc.DefaultClientOpts(), s.TransferChannelOptions(), t.Name()) + s.StartRelayer(relayer, testName) + + chanAB := s.GetChainAToChainBChannel(testName) + chanBC := s.GetChainBToChainCChannel(testName) + chanCD := s.GetChainCToChainDChannel(testName) + + ab, err := query.Channel(ctx, chainA, transfertypes.PortID, chanAB.ChannelID) + s.Require().NoError(err) + s.Require().NotNil(ab) + + bc, err := query.Channel(ctx, chainB, transfertypes.PortID, chanBC.ChannelID) + s.Require().NoError(err) + s.Require().NotNil(bc) + + cd, err := query.Channel(ctx, chainC, transfertypes.PortID, chanCD.ChannelID) + s.Require().NoError(err) + s.Require().NotNil(cd) + + escrowAddrA := transfertypes.GetEscrowAddress(chanAB.PortID, chanAB.ChannelID) + escrowAddrB := transfertypes.GetEscrowAddress(chanCD.PortID, chanCD.ChannelID) + escrowAddrC := escrowAddrB + escrowAddrD := userD.FormattedAddress() + + denomA := chainA.Config().Denom + ibcTokenB := testsuite.GetIBCToken(denomA, chanAB.PortID, chanAB.ChannelID) + ibcTokenC := testsuite.GetIBCToken(ibcTokenB.Path(), chanAB.Counterparty.PortID, chanCD.Counterparty.ChannelID) + ibcTokenD := testsuite.GetIBCToken(ibcTokenC.Path(), chanCD.Counterparty.PortID, chanCD.Counterparty.ChannelID) + + t.Run("Multihop forward [A -> B -> C -> D]", func(_ *testing.T) { + // Send packet from Chain A->Chain B->Chain C->Chain D + // From A -> B will be handled by transfer msg. + // From B -> C will be handled by firstHopMetadata. + // From C -> D will be handled by secondHopMetadata. + secondHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userD.FormattedAddress(), + Channel: chanCD.ChannelID, + Port: chanCD.PortID, + }, + } + nextBz, err := json.Marshal(secondHopMetadata) + s.Require().NoError(err) + next := string(nextBz) + + firstHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userC.FormattedAddress(), + Channel: chanBC.ChannelID, + Port: chanBC.PortID, + Next: &next, + }, + } + + memo, err := json.Marshal(firstHopMetadata) + s.Require().NoError(err) + + bHeight, err := chainB.Height(ctx) + s.Require().NoError(err) + + txResp := s.Transfer(ctx, chainA, userA, chanAB.PortID, chanAB.ChannelID, testvalues.DefaultTransferAmount(denomA), userA.FormattedAddress(), userB.FormattedAddress(), s.GetTimeoutHeight(ctx, chainA), 0, string(memo)) + s.AssertTxSuccess(txResp) + + packet, err := ibctesting.ParsePacketFromEvents(txResp.Events) + s.Require().NoError(err) + s.Require().NotNil(packet) + + _, err = cosmos.PollForMessage[*chantypes.MsgRecvPacket](ctx, chainB.(*cosmos.CosmosChain), cosmos.DefaultEncoding().InterfaceRegistry, bHeight, bHeight+40, nil) + s.Require().NoError(err) + + actualBalance, err := s.GetChainANativeBalance(ctx, userA) + s.Require().NoError(err) + expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount + s.Require().Equal(expected, actualBalance) + + escrowBalA, err := query.Balance(ctx, chainA, escrowAddrA.String(), denomA) + s.Require().NoError(err) + s.Require().Equal(testvalues.IBCTransferAmount, escrowBalA.Int64()) + + s.Require().Eventually(func() bool { + _, err := query.GRPCQuery[chantypes.QueryPacketCommitmentResponse](ctx, chainA, &chantypes.QueryPacketCommitmentRequest{ + PortId: chanAB.PortID, + ChannelId: chanAB.ChannelID, + Sequence: packet.Sequence, + }) + return err != nil && strings.Contains(err.Error(), "packet commitment hash not found") + }, time.Second*70, time.Second) + + versionB := chainB.Config().Images[0].Version + if testvalues.TokenMetadataFeatureReleases.IsSupported(versionB) { + s.AssertHumanReadableDenom(ctx, chainB, denomA, chanAB) + } + + escrowBalB, err := query.Balance(ctx, chainB, escrowAddrB.String(), ibcTokenB.IBCDenom()) + s.Require().NoError(err) + s.Require().Equal(testvalues.IBCTransferAmount, escrowBalB.Int64()) + + escrowBalC, err := query.Balance(ctx, chainC, escrowAddrC.String(), ibcTokenC.IBCDenom()) + s.Require().NoError(err) + s.Require().Equal(testvalues.IBCTransferAmount, escrowBalC.Int64()) + + balanceD, err := query.Balance(ctx, chainD, escrowAddrD, ibcTokenD.IBCDenom()) + s.Require().NoError(err) + s.Require().Equal(testvalues.IBCTransferAmount, balanceD.Int64()) + }) + + t.Run("Packet forwarded [D -> C -> B -> A]", func(_ *testing.T) { + secondHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userA.FormattedAddress(), + Channel: chanAB.Counterparty.ChannelID, + Port: chanAB.Counterparty.PortID, + }, + } + nextBz, err := json.Marshal(secondHopMetadata) + s.Require().NoError(err) + next := string(nextBz) + + firstHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userB.FormattedAddress(), + Channel: chanBC.Counterparty.ChannelID, + Port: chanBC.Counterparty.PortID, + Next: &next, + }, + } + + memo, err := json.Marshal(firstHopMetadata) + s.Require().NoError(err) + + cHeight, err := chainC.Height(ctx) + s.Require().NoError(err) + + txResp := s.Transfer(ctx, chainD, userD, chanCD.Counterparty.PortID, chanCD.Counterparty.ChannelID, testvalues.DefaultTransferAmount(ibcTokenD.IBCDenom()), userD.FormattedAddress(), userC.FormattedAddress(), s.GetTimeoutHeight(ctx, chainD), 0, string(memo)) + s.AssertTxSuccess(txResp) + + packet, err := ibctesting.ParsePacketFromEvents(txResp.Events) + s.Require().NoError(err) + s.Require().NotNil(packet) + + _, err = cosmos.PollForMessage[*chantypes.MsgRecvPacket](ctx, chainC.(*cosmos.CosmosChain), cosmos.DefaultEncoding().InterfaceRegistry, cHeight, cHeight+40, nil) + s.Require().NoError(err) + + s.Require().Eventually(func() bool { + _, err := query.GRPCQuery[chantypes.QueryPacketCommitmentResponse](ctx, chainD, &chantypes.QueryPacketCommitmentRequest{ + PortId: chanCD.Counterparty.PortID, + ChannelId: chanCD.Counterparty.ChannelID, + Sequence: packet.Sequence, + }) + return err != nil && strings.Contains(err.Error(), "packet commitment hash not found") + }, time.Second*70, time.Second) + + // All escrow accounts have been cleared + escrowBalA, err := query.Balance(ctx, chainA, escrowAddrA.String(), denomA) + s.Require().NoError(err) + s.Require().Zero(escrowBalA.Int64()) + + escrowBalB, err := query.Balance(ctx, chainB, escrowAddrB.String(), ibcTokenB.IBCDenom()) + s.Require().NoError(err) + s.Require().Zero(escrowBalB.Int64()) + + escrowBalC, err := query.Balance(ctx, chainC, escrowAddrC.String(), ibcTokenC.IBCDenom()) + s.Require().NoError(err) + s.Require().Zero(escrowBalC.Int64()) + + escrowBalD, err := query.Balance(ctx, chainD, userD.FormattedAddress(), ibcTokenD.IBCDenom()) + s.Require().NoError(err) + s.Require().Zero(escrowBalD.Int64()) + + // User A has his asset back + balance, err := s.GetChainANativeBalance(ctx, userA) + s.Require().NoError(err) + s.Require().Equal(testvalues.StartingTokenAmount, balance) + }) + + t.Run("Error while forwarding: Refund ok [A -> B -> C ->X D]", func(_ *testing.T) { + secondHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: "GurbageAddress", + Channel: chanCD.ChannelID, + Port: chanCD.PortID, + }, + } + nextBz, err := json.Marshal(secondHopMetadata) + s.Require().NoError(err) + next := string(nextBz) + + firstHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userC.FormattedAddress(), + Channel: chanBC.ChannelID, + Port: chanBC.PortID, + Next: &next, + }, + } + + memo, err := json.Marshal(firstHopMetadata) + s.Require().NoError(err) + + txResp := s.Transfer(ctx, chainA, userA, chanAB.PortID, chanAB.ChannelID, testvalues.DefaultTransferAmount(ibcTokenD.IBCDenom()), userA.FormattedAddress(), userB.FormattedAddress(), s.GetTimeoutHeight(ctx, chainA), 0, string(memo)) + s.AssertTxFailure(txResp, transfertypes.ErrDenomNotFound) + + _, err = ibctesting.ParsePacketFromEvents(txResp.Events) + s.Require().ErrorContains(err, "acknowledgement event attribute not found") + + // C -> D should not happen. + // Refunded UserA on chain A. + escrowBalA, err := query.Balance(ctx, chainA, escrowAddrA.String(), denomA) + s.Require().NoError(err) + s.Require().Zero(escrowBalA.Int64()) + + escrowBalB, err := query.Balance(ctx, chainB, escrowAddrB.String(), ibcTokenB.IBCDenom()) + s.Require().NoError(err) + s.Require().Zero(escrowBalB.Int64()) + + escrowBalC, err := query.Balance(ctx, chainC, escrowAddrC.String(), ibcTokenC.IBCDenom()) + s.Require().NoError(err) + s.Require().Zero(escrowBalC.Int64()) + + escrowBalD, err := query.Balance(ctx, chainD, userD.FormattedAddress(), ibcTokenD.IBCDenom()) + s.Require().NoError(err) + s.Require().Zero(escrowBalD.Int64()) + + // User A has his asset back + balance, err := s.GetChainANativeBalance(ctx, userA) + s.Require().NoError(err) + s.Require().Equal(testvalues.StartingTokenAmount, balance) + + // 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. + + denomB := chainB.Config().Denom + ibcTokenA := testsuite.GetIBCToken(denomB, chanAB.Counterparty.PortID, chanAB.Counterparty.ChannelID) + escrowAddrB = transfertypes.GetEscrowAddress(chanAB.Counterparty.PortID, chanAB.Counterparty.ChannelID) + + txResp = s.Transfer(ctx, chainB, userB, chanAB.Counterparty.PortID, chanAB.Counterparty.ChannelID, testvalues.DefaultTransferAmount(denomB), userB.FormattedAddress(), userA.FormattedAddress(), s.GetTimeoutHeight(ctx, chainB), 0, "") + s.AssertTxSuccess(txResp) + + escrowBalB, err = query.Balance(ctx, chainB, escrowAddrB.String(), denomB) + s.Require().NoError(err) + s.Require().Equal(escrowBalB.Int64(), testvalues.IBCTransferAmount) + + balanceA, err := query.Balance(ctx, chainA, userA.FormattedAddress(), ibcTokenA.IBCDenom()) + s.Require().NoError(err) + s.Require().Equal(balanceA.Int64(), testvalues.IBCTransferAmount) + + // Proof that unwinding happens. + txResp = s.Transfer(ctx, chainA, userA, chanAB.PortID, chanAB.ChannelID, testvalues.DefaultTransferAmount(ibcTokenA.IBCDenom()), userA.FormattedAddress(), userB.FormattedAddress(), s.GetTimeoutHeight(ctx, chainA), 0, "") + s.AssertTxSuccess(txResp) + + // Escrow account is cleared on chain B + escrowBalB, err = query.Balance(ctx, chainB, escrowAddrB.String(), denomB) + s.Require().NoError(err) + s.Require().Zero(escrowBalB.Int64()) + + // ChainB user now has the same amount he started with + balanceB, err := s.GetChainBNativeBalance(ctx, userB) + s.Require().NoError(err) + s.Require().Equal(testvalues.StartingTokenAmount, balanceB) + }) + + // A -> B -> A Nothing changes + t.Run("A -> B -> A", func(_ *testing.T) { + balanceAInt, err := s.GetChainANativeBalance(ctx, userA) + s.Require().NoError(err) + balanceBInt, err := s.GetChainBNativeBalance(ctx, userB) + s.Require().NoError(err) + + firstHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userA.FormattedAddress(), + Channel: chanAB.Counterparty.ChannelID, + Port: chanAB.Counterparty.PortID, + }, + } + + memo, err := json.Marshal(firstHopMetadata) + s.Require().NoError(err) + + txResp := s.Transfer(ctx, chainA, userA, chanAB.PortID, chanAB.ChannelID, testvalues.DefaultTransferAmount(denomA), userA.FormattedAddress(), userB.FormattedAddress(), s.GetTimeoutHeight(ctx, chainA), 0, string(memo)) + s.AssertTxSuccess(txResp) + + balanceAIntAfter, err := s.GetChainANativeBalance(ctx, userA) + s.Require().NoError(err) + balanceBIntAfter, err := s.GetChainBNativeBalance(ctx, userB) + s.Require().NoError(err) + + s.Require().Equal(balanceAInt, balanceAIntAfter) + s.Require().Equal(balanceBInt, balanceBIntAfter) + }) +} diff --git a/e2e/tests/packet_forward_middleware/pfm_upgrade_test.go b/e2e/tests/packet_forward_middleware/pfm_upgrade_test.go new file mode 100644 index 00000000000..68d9ee7b475 --- /dev/null +++ b/e2e/tests/packet_forward_middleware/pfm_upgrade_test.go @@ -0,0 +1,180 @@ +//go:build !test_e2e + +package pfm + +import ( + "context" + "encoding/json" + "strings" + "testing" + "time" + + "github.com/strangelove-ventures/interchaintest/v8/chain/cosmos" + "github.com/strangelove-ventures/interchaintest/v8/ibc" + test "github.com/strangelove-ventures/interchaintest/v8/testutil" + + "github.com/cosmos/ibc-go/e2e/testsuite" + "github.com/cosmos/ibc-go/e2e/testsuite/query" + "github.com/cosmos/ibc-go/e2e/testvalues" + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + chantypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" + ibctesting "github.com/cosmos/ibc-go/v10/testing" +) + +// TODO: Move to `e2e/tests/upgrades` in #8360 +type PFMUpgradeTestSuite struct { + testsuite.E2ETestSuite +} + +func TestPFMUpgradeTestSuite(t *testing.T) { + // TODO: Enable as we clean up these tests #8360 + t.Skip("Skipping as relayer is not relaying failed packets") + testCfg := testsuite.LoadConfig() + if testCfg.UpgradePlanName == "" { + t.Fatalf("%s must be set when running an upgrade test", testsuite.ChainUpgradePlanEnv) + } + + // testifysuite.Run(t, new(PFMUpgradeTestSuite)) +} + +func updateGenesisChainB(option *testsuite.ChainOptions) { + option.ChainSpecs[1].ModifyGenesis = cosmos.ModifyGenesis([]cosmos.GenesisKV{ + { + Key: "app_state.gov.params.voting_period", + Value: "15s", + }, + { + Key: "app_state.gov.params.max_deposit_period", + Value: "10s", + }, + { + Key: "app_state.gov.params.min_deposit.0.denom", + Value: "ustake", + }, + }) +} + +func (s *PFMUpgradeTestSuite) SetupSuite() { + s.SetupChains(context.TODO(), 4, nil, updateGenesisChainB) +} + +func (s *PFMUpgradeTestSuite) TestV8ToV10ChainUpgrade_PacketForward() { + t := s.T() + ctx := context.TODO() + testName := t.Name() + + chains := s.GetAllChains() + chainA, chainB, chainC, _ := chains[0], chains[1], chains[2], chains[3] + + userA := s.CreateUserOnChainA(ctx, testvalues.StartingTokenAmount) + userB := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) + userC := s.CreateUserOnChainC(ctx, testvalues.StartingTokenAmount) + + relayer := s.CreatePaths(ibc.DefaultClientOpts(), s.TransferChannelOptions(), t.Name()) + s.StartRelayer(relayer, testName) + + chanAB := s.GetChainAToChainBChannel(testName) + chanBC := s.GetChainBToChainCChannel(testName) + chanCD := s.GetChainCToChainDChannel(testName) + + ab, err := query.Channel(ctx, chainA, transfertypes.PortID, chanAB.ChannelID) + s.Require().NoError(err) + s.Require().NotNil(ab) + + bc, err := query.Channel(ctx, chainB, transfertypes.PortID, chanBC.ChannelID) + s.Require().NoError(err) + s.Require().NotNil(bc) + + cd, err := query.Channel(ctx, chainC, transfertypes.PortID, chanCD.ChannelID) + s.Require().NoError(err) + s.Require().NotNil(cd) + + escrowAddrA := transfertypes.GetEscrowAddress(chanAB.PortID, chanAB.ChannelID) + + denomB := chainB.Config().Denom + ibcTokenA := testsuite.GetIBCToken(denomB, chanAB.Counterparty.PortID, chanAB.Counterparty.ChannelID) + + s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA, chainB), "failed to wait for blocks") + + t.Run("Send from B -> A", func(_ *testing.T) { + aHeight, err := chainA.Height(ctx) + s.Require().NoError(err) + + txResp := s.Transfer(ctx, chainB, userB, chanAB.Counterparty.PortID, chanAB.Counterparty.ChannelID, testvalues.DefaultTransferAmount(denomB), userB.FormattedAddress(), userA.FormattedAddress(), s.GetTimeoutHeight(ctx, chainB), 0, "") + s.AssertTxSuccess(txResp) + + bBal, err := s.GetChainBNativeBalance(ctx, userB) + s.Require().NoError(err) + expected := testvalues.StartingTokenAmount - testvalues.IBCTransferAmount + s.Require().Equal(expected, bBal) + + _, err = cosmos.PollForMessage[*chantypes.MsgRecvPacket](ctx, chainA.(*cosmos.CosmosChain), cosmos.DefaultEncoding().InterfaceRegistry, aHeight, aHeight+40, nil) + s.Require().NoError(err) + + escrowBalB, err := query.Balance(ctx, chainB, escrowAddrA.String(), denomB) + s.Require().NoError(err) + s.Require().Equal(testvalues.IBCTransferAmount, escrowBalB.Int64()) + + escrowBalA, err := query.Balance(ctx, chainA, userA.FormattedAddress(), ibcTokenA.IBCDenom()) + s.Require().NoError(err) + s.Require().Equal(testvalues.IBCTransferAmount, escrowBalA.Int64()) + }) + + // Send the IBC denom that chain A received from the previous step + t.Run("Send from A -> B -> C ->X D", func(_ *testing.T) { + secondHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: "cosmos1wgz9ntx6e5vu4npeabcde88d7kfsymag62p6y2", + Channel: chanCD.ChannelID, + Port: chanCD.PortID, + }, + } + nextBz, err := json.Marshal(secondHopMetadata) + s.Require().NoError(err) + next := string(nextBz) + + firstHopMetadata := &PacketMetadata{ + Forward: &ForwardMetadata{ + Receiver: userC.FormattedAddress(), + Channel: chanBC.ChannelID, + Port: chanBC.PortID, + Next: &next, + }, + } + + memo, err := json.Marshal(firstHopMetadata) + s.Require().NoError(err) + + bHeight, err := chainB.Height(ctx) + s.Require().NoError(err) + + ibcDenomOnA := ibcTokenA.IBCDenom() + txResp := s.Transfer(ctx, chainA, userA, chanAB.PortID, chanAB.ChannelID, testvalues.DefaultTransferAmount(ibcDenomOnA), userA.FormattedAddress(), userB.FormattedAddress(), s.GetTimeoutHeight(ctx, chainA), 0, string(memo)) + s.AssertTxSuccess(txResp) + + packet, err := ibctesting.ParsePacketFromEvents(txResp.Events) + s.Require().NoError(err) + s.Require().NotNil(packet) + + _, err = cosmos.PollForMessage[*chantypes.MsgRecvPacket](ctx, chainB.(*cosmos.CosmosChain), cosmos.DefaultEncoding().InterfaceRegistry, bHeight, bHeight+40, nil) + s.Require().NoError(err) + + actualBalance, err := query.Balance(ctx, chainA, userA.FormattedAddress(), ibcDenomOnA) + s.Require().NoError(err) + s.Require().Zero(actualBalance) + + escrowBalA, err := query.Balance(ctx, chainA, escrowAddrA.String(), ibcDenomOnA) + s.Require().NoError(err) + s.Require().Equal(testvalues.IBCTransferAmount, escrowBalA.Int64()) + + // Assart Packet relayed + s.Require().Eventually(func() bool { + _, err := query.GRPCQuery[chantypes.QueryPacketCommitmentResponse](ctx, chainA, &chantypes.QueryPacketCommitmentRequest{ + PortId: chanAB.PortID, + ChannelId: chanAB.ChannelID, + Sequence: packet.Sequence, + }) + return err != nil && strings.Contains(err.Error(), "packet commitment hash not found") + }, time.Second*70, time.Second) + }) +} diff --git a/e2e/tests/transfer/authz_test.go b/e2e/tests/transfer/authz_test.go index 4f64f4e5e7e..fb66f95f2d2 100644 --- a/e2e/tests/transfer/authz_test.go +++ b/e2e/tests/transfer/authz_test.go @@ -39,7 +39,7 @@ func (s *AuthzTransferTestSuite) SetupSuite() { } func (s *AuthzTransferTestSuite) CreateAuthzTestPath(testName string) (ibc.Relayer, ibc.ChannelOutput) { - return s.CreatePaths(ibc.DefaultClientOpts(), s.TransferChannelOptions(), testName), s.GetChainAChannelForTest(testName) + return s.CreatePaths(ibc.DefaultClientOpts(), s.TransferChannelOptions(), testName), s.GetChainAToChainBChannel(testName) } // QueryGranterGrants returns all GrantAuthorizations for the given granterAddress. diff --git a/e2e/tests/transfer/base_test.go b/e2e/tests/transfer/base_test.go index b8990d5d153..62355678ec2 100644 --- a/e2e/tests/transfer/base_test.go +++ b/e2e/tests/transfer/base_test.go @@ -51,7 +51,7 @@ func (s *transferTester) QueryTransferParams(ctx context.Context, chain ibc.Chai // CreateTransferPath sets up a path between chainA and chainB with a transfer channel and returns the relayer wired // up to watch the channel and port IDs created. func (s *transferTester) CreateTransferPath(testName string) (ibc.Relayer, ibc.ChannelOutput) { - relayer, channel := s.CreatePaths(ibc.DefaultClientOpts(), s.TransferChannelOptions(), testName), s.GetChainAChannelForTest(testName) + relayer, channel := s.CreatePaths(ibc.DefaultClientOpts(), s.TransferChannelOptions(), testName), s.GetChainAToChainBChannel(testName) s.T().Logf("test %s running on portID %s channelID %s", testName, channel.PortID, channel.ChannelID) return relayer, channel } diff --git a/e2e/tests/transfer/send_enabled_test.go b/e2e/tests/transfer/send_enabled_test.go index fe2b7e1810b..6ee7f374122 100644 --- a/e2e/tests/transfer/send_enabled_test.go +++ b/e2e/tests/transfer/send_enabled_test.go @@ -45,7 +45,7 @@ func (s *TransferTestSuiteSendEnabled) TestSendEnabledParam() { chainA, chainB := s.GetChains() - channelA := s.GetChainAChannelForTest(testName) + channelA := s.GetChainAToChainBChannel(testName) chainAVersion := chainA.Config().Images[0].Version chainADenom := chainA.Config().Denom diff --git a/e2e/tests/transfer/send_receive_test.go b/e2e/tests/transfer/send_receive_test.go index b900dcb9f40..a2807f4c203 100644 --- a/e2e/tests/transfer/send_receive_test.go +++ b/e2e/tests/transfer/send_receive_test.go @@ -46,7 +46,7 @@ func (s *TransferTestSuiteSendReceive) TestReceiveEnabledParam() { chainA, chainB := s.GetChains() relayer := s.GetRelayerForTest(testName) - channelA := s.GetChainAChannelForTest(testName) + channelA := s.GetChainAToChainBChannel(testName) chainAVersion := chainA.Config().Images[0].Version diff --git a/e2e/tests/upgrades/genesis_test.go b/e2e/tests/upgrades/genesis_test.go index 1299eb0158e..81e2b2218ea 100644 --- a/e2e/tests/upgrades/genesis_test.go +++ b/e2e/tests/upgrades/genesis_test.go @@ -13,7 +13,6 @@ import ( "github.com/strangelove-ventures/interchaintest/v8/ibc" test "github.com/strangelove-ventures/interchaintest/v8/testutil" "github.com/stretchr/testify/suite" - "go.uber.org/zap" sdkmath "cosmossdk.io/math" @@ -62,15 +61,13 @@ func (s *GenesisTestSuite) SetupSuite() { func (s *GenesisTestSuite) TestIBCGenesis() { t := s.T() - haltHeight := int64(100) - chainA, chainB := s.GetChains() ctx := context.Background() testName := t.Name() relayer := s.CreateDefaultPaths(testName) - channelA := s.GetChainAChannelForTest(testName) + channelA := s.GetChainAToChainBChannel(testName) var ( chainADenom = chainA.Config().Denom @@ -83,7 +80,7 @@ func (s *GenesisTestSuite) TestIBCGenesis() { chainBWallet := s.CreateUserOnChainB(ctx, testvalues.StartingTokenAmount) chainBAddress := chainBWallet.FormattedAddress() - s.Require().NoError(test.WaitForBlocks(ctx, 1, chainA, chainB), "failed to wait for blocks") + s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB), "failed to wait for blocks") t.Run("ics20: native IBC token transfer from chainA to chainB, sender is source of tokens", func(t *testing.T) { transferTxResp := s.Transfer(ctx, chainA, chainAWallet, channelA.PortID, channelA.ChannelID, testvalues.DefaultTransferAmount(chainADenom), chainAAddress, chainBAddress, s.GetTimeoutHeight(ctx, chainB), 0, "") @@ -150,7 +147,7 @@ func (s *GenesisTestSuite) TestIBCGenesis() { s.Require().NoError(test.WaitForBlocks(ctx, 10, chainA, chainB), "failed to wait for blocks") t.Run("Halt chain and export genesis", func(t *testing.T) { - s.HaltChainAndExportGenesis(ctx, chainA.(*cosmos.CosmosChain), haltHeight) + s.HaltChainAndExportGenesis(ctx, chainA.(*cosmos.CosmosChain)) }) t.Run("ics20: native IBC token transfer from chainA to chainB, sender is source of tokens", func(t *testing.T) { @@ -213,39 +210,25 @@ func (s *GenesisTestSuite) TestIBCGenesis() { s.Require().NoError(test.WaitForBlocks(ctx, 5, chainA, chainB), "failed to wait for blocks") } -func (s *GenesisTestSuite) HaltChainAndExportGenesis(ctx context.Context, chain *cosmos.CosmosChain, haltHeight int64) { +func (s *GenesisTestSuite) HaltChainAndExportGenesis(ctx context.Context, chain *cosmos.CosmosChain) { timeoutCtx, timeoutCtxCancel := context.WithTimeout(ctx, time.Minute*2) defer timeoutCtxCancel() - err := test.WaitForBlocks(timeoutCtx, int(haltHeight), chain) - s.Require().Error(err, "chain did not halt at halt height") + beforeHaltHeight, err := chain.Height(timeoutCtx) + s.Require().NoError(err, "error fetching height before halt") + + err = test.WaitForBlocks(timeoutCtx, 1, chain) + s.Require().NoError(err, "failed to wait for blocks") err = chain.StopAllNodes(ctx) s.Require().NoError(err, "error stopping node(s)") - state, err := chain.ExportState(ctx, haltHeight) + state, err := chain.ExportState(ctx, beforeHaltHeight) s.Require().NoError(err) - appTomlOverrides := make(test.Toml) - - appTomlOverrides["halt-height"] = 0 - for _, node := range chain.Nodes() { err := node.OverwriteGenesisFile(ctx, []byte(state)) s.Require().NoError(err) - } - - for _, node := range chain.Nodes() { - err := test.ModifyTomlConfigFile( - ctx, - zap.NewExample(), - node.DockerClient, - node.TestName, - node.VolumeName, - "config/app.toml", - appTomlOverrides, - ) - s.Require().NoError(err) _, _, err = node.ExecBin(ctx, "comet", "unsafe-reset-all") s.Require().NoError(err) @@ -263,5 +246,5 @@ func (s *GenesisTestSuite) HaltChainAndExportGenesis(ctx context.Context, chain height, err := chain.Height(ctx) s.Require().NoError(err, "error fetching height after halt") - s.Require().Greater(height, haltHeight, "height did not increment after halt") + s.Require().Greater(height, beforeHaltHeight+1, "height did not increment after halt") } diff --git a/e2e/tests/upgrades/upgrade_test.go b/e2e/tests/upgrades/upgrade_test.go index f30976dc959..1707ed829bf 100644 --- a/e2e/tests/upgrades/upgrade_test.go +++ b/e2e/tests/upgrades/upgrade_test.go @@ -61,7 +61,7 @@ func (s *UpgradeTestSuite) SetupSuite() { } func (s *UpgradeTestSuite) CreateUpgradeTestPath(testName string) (ibc.Relayer, ibc.ChannelOutput) { - return s.CreatePaths(ibc.DefaultClientOpts(), s.TransferChannelOptions(), testName), s.GetChainAChannelForTest(testName) + return s.CreatePaths(ibc.DefaultClientOpts(), s.TransferChannelOptions(), testName), s.GetChainAToChainBChannel(testName) } // UpgradeChain upgrades a chain to a specific version using the planName provided. @@ -660,7 +660,7 @@ func (s *UpgradeTestSuite) TestV8ToV8_1ChainUpgrade() { testName := t.Name() relayer := s.CreatePaths(ibc.DefaultClientOpts(), s.TransferChannelOptions(), testName) - channelA := s.GetChainAChannelForTest(testName) + channelA := s.GetChainAToChainBChannel(testName) chainA, chainB := s.GetChains() chainADenom := chainA.Config().Denom diff --git a/e2e/testsuite/codec.go b/e2e/testsuite/codec.go index 7c2679919e5..303acb7ecc0 100644 --- a/e2e/testsuite/codec.go +++ b/e2e/testsuite/codec.go @@ -26,6 +26,7 @@ import ( wasmtypes "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10/types" icacontrollertypes "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/controller/types" icahosttypes "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/host/types" + packetforwardtypes "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/types" transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" v7migrations "github.com/cosmos/ibc-go/v10/modules/core/02-client/migrations/v7" clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" @@ -71,6 +72,7 @@ func codecAndEncodingConfig() (*codec.ProtoCodec, testutil.TestEncodingConfig) { ibctmtypes.RegisterInterfaces(cfg.InterfaceRegistry) wasmtypes.RegisterInterfaces(cfg.InterfaceRegistry) channeltypesv2.RegisterInterfaces(cfg.InterfaceRegistry) + packetforwardtypes.RegisterInterfaces(cfg.InterfaceRegistry) // all other types upgradetypes.RegisterInterfaces(cfg.InterfaceRegistry) diff --git a/e2e/testsuite/testconfig.go b/e2e/testsuite/testconfig.go index a9f1121b292..da372cc519e 100644 --- a/e2e/testsuite/testconfig.go +++ b/e2e/testsuite/testconfig.go @@ -702,7 +702,7 @@ type ChainOptions struct { // ChainOptionConfiguration enables arbitrary configuration of ChainOptions. type ChainOptionConfiguration func(options *ChainOptions) -// DefaultChainOptions returns the default configuration for the chains. +// DefaultChainOptions returns the default configuration for required number of chains. // These options can be configured by passing configuration functions to E2ETestSuite.GetChains. func DefaultChainOptions(chainCount int) (ChainOptions, error) { tc := LoadConfig() diff --git a/e2e/testsuite/testsuite.go b/e2e/testsuite/testsuite.go index eb520cc8c5c..f60ceb497a3 100644 --- a/e2e/testsuite/testsuite.go +++ b/e2e/testsuite/testsuite.go @@ -300,13 +300,23 @@ func getLatestChannel(channels []ibc.ChannelOutput) ibc.ChannelOutput { }) } -// GetChainAChannelForTest returns the ibc.ChannelOutput for the current test. +// GetChainAToChainBChannel returns the ibc.ChannelOutput for the current test. // this defaults to the first entry in the list, and will be what is needed in the case of // a single channel test. -func (s *E2ETestSuite) GetChainAChannelForTest(testName string) ibc.ChannelOutput { +func (s *E2ETestSuite) GetChainAToChainBChannel(testName string) ibc.ChannelOutput { return s.GetChannelsForTest(s.GetAllChains()[0], testName)[0] } +// GetChainBToChainCChannel returns the ibc.ChannelOutput for the current test. +func (s *E2ETestSuite) GetChainBToChainCChannel(testName string) ibc.ChannelOutput { + return s.GetChannelsForTest(s.GetAllChains()[1], testName)[1] +} + +// GetChainCToChainDChannel returns the ibc.ChannelOutput for the current test. +func (s *E2ETestSuite) GetChainCToChainDChannel(testName string) ibc.ChannelOutput { + return s.GetChannelsForTest(s.GetAllChains()[2], testName)[1] +} + // GetChannelsForTest returns all channels for the specified test. func (s *E2ETestSuite) GetChannelsForTest(chain ibc.Chain, testName string) []ibc.ChannelOutput { channels, ok := s.channels[testName][chain] @@ -553,6 +563,11 @@ func (s *E2ETestSuite) CreateUserOnChainC(ctx context.Context, amount int64) ibc return s.createWalletOnChainIndex(ctx, amount, 2) } +// CreateUserOnChainD creates a user with the given amount of funds on chain C. +func (s *E2ETestSuite) CreateUserOnChainD(ctx context.Context, amount int64) ibc.Wallet { + return s.createWalletOnChainIndex(ctx, amount, 3) +} + // createWalletOnChainIndex creates a wallet with the given amount of funds on the chain of the given index. func (s *E2ETestSuite) createWalletOnChainIndex(ctx context.Context, amount, chainIndex int64) ibc.Wallet { chain := s.GetAllChains()[chainIndex] @@ -576,15 +591,11 @@ func (s *E2ETestSuite) GetChainBNativeBalance(ctx context.Context, user ibc.Wall // GetChainBalanceForDenom returns the balance for a given denom given a chain. func GetChainBalanceForDenom(ctx context.Context, chain ibc.Chain, denom string, user ibc.Wallet) (int64, error) { - balanceResp, err := query.GRPCQuery[banktypes.QueryBalanceResponse](ctx, chain, &banktypes.QueryBalanceRequest{ - Address: user.FormattedAddress(), - Denom: denom, - }) + resp, err := query.Balance(ctx, chain, user.FormattedAddress(), denom) if err != nil { return 0, err } - - return balanceResp.Balance.Amount.Int64(), nil + return resp.Int64(), nil } // AssertPacketRelayed asserts that the packet commitment does not exist on the sending chain. diff --git a/go.mod b/go.mod index 575ce4c6cfe..4df1cffcda6 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/golang/protobuf v1.5.4 github.com/grpc-ecosystem/grpc-gateway v1.16.0 github.com/hashicorp/go-metrics v0.5.4 + github.com/iancoleman/orderedmap v0.3.0 github.com/spf13/cast v1.8.0 github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.10.0 diff --git a/go.sum b/go.sum index 5e8574614d0..2e0c3c01a05 100644 --- a/go.sum +++ b/go.sum @@ -1186,6 +1186,8 @@ github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0Jr github.com/huandu/skiplist v1.2.1 h1:dTi93MgjwErA/8idWTzIw4Y1kZsMWx35fmI2c8Rij7w= github.com/huandu/skiplist v1.2.1/go.mod h1:7v3iFjLcSAzO4fN5B8dvebvo/qsfumiLiDXMrPiHF9w= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= diff --git a/modules/apps/callbacks/ibc_middleware.go b/modules/apps/callbacks/ibc_middleware.go index 3c5e84be64d..95978407312 100644 --- a/modules/apps/callbacks/ibc_middleware.go +++ b/modules/apps/callbacks/ibc_middleware.go @@ -22,7 +22,7 @@ var ( // IBCMiddleware implements the ICS26 callbacks for the ibc-callbacks middleware given // the underlying application. type IBCMiddleware struct { - app types.CallbacksCompatibleModule + app porttypes.PacketUnmarshalarModule ics4Wrapper porttypes.ICS4Wrapper contractKeeper types.ContractKeeper @@ -40,9 +40,9 @@ func NewIBCMiddleware( app porttypes.IBCModule, ics4Wrapper porttypes.ICS4Wrapper, contractKeeper types.ContractKeeper, maxCallbackGas uint64, ) IBCMiddleware { - packetDataUnmarshalerApp, ok := app.(types.CallbacksCompatibleModule) + packetDataUnmarshalerApp, ok := app.(porttypes.PacketUnmarshalarModule) if !ok { - panic(fmt.Errorf("underlying application does not implement %T", (*types.CallbacksCompatibleModule)(nil))) + panic(fmt.Errorf("underlying application does not implement %T", (*porttypes.PacketUnmarshalarModule)(nil))) } if ics4Wrapper == nil { diff --git a/modules/apps/callbacks/ibc_middleware_test.go b/modules/apps/callbacks/ibc_middleware_test.go index 4aa7f87bb92..7950879d44e 100644 --- a/modules/apps/callbacks/ibc_middleware_test.go +++ b/modules/apps/callbacks/ibc_middleware_test.go @@ -44,7 +44,7 @@ func (s *CallbacksTestSuite) TestNewIBCMiddleware() { func() { _ = ibccallbacks.NewIBCMiddleware(nil, &channelkeeper.Keeper{}, simapp.ContractKeeper{}, maxCallbackGas) }, - fmt.Errorf("underlying application does not implement %T", (*types.CallbacksCompatibleModule)(nil)), + fmt.Errorf("underlying application does not implement %T", (*porttypes.PacketUnmarshalarModule)(nil)), }, { "panics with nil contract keeper", @@ -998,7 +998,7 @@ func (s *CallbacksTestSuite) TestUnmarshalPacketDataV1() { transferStack, ok := s.chainA.App.GetIBCKeeper().PortKeeper.Route(transfertypes.ModuleName) s.Require().True(ok) - unmarshalerStack, ok := transferStack.(types.CallbacksCompatibleModule) + unmarshalerStack, ok := transferStack.(porttypes.PacketUnmarshalarModule) s.Require().True(ok) expPacketDataICS20V1 := transfertypes.FungibleTokenPacketData{ diff --git a/modules/apps/callbacks/types/callbacks.go b/modules/apps/callbacks/types/callbacks.go index edb7b1e7ba1..cdd836632c0 100644 --- a/modules/apps/callbacks/types/callbacks.go +++ b/modules/apps/callbacks/types/callbacks.go @@ -8,7 +8,6 @@ import ( channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" porttypes "github.com/cosmos/ibc-go/v10/modules/core/05-port/types" - "github.com/cosmos/ibc-go/v10/modules/core/api" ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" ) @@ -43,20 +42,6 @@ keeper to verify that the packet sender is the same as the callback address if d */ -// CallbacksCompatibleModule is an interface that combines the IBCModule and PacketDataUnmarshaler -// interfaces to assert that the underlying application supports both. -type CallbacksCompatibleModule interface { - porttypes.IBCModule - porttypes.PacketDataUnmarshaler -} - -// CallbacksCompatibleModuleV2 is an interface that combines the IBCModuleV2 and PacketDataUnmarshaler -// interfaces to assert that the underlying application supports both. -type CallbacksCompatibleModuleV2 interface { - api.IBCModule - api.PacketDataUnmarshaler -} - // CallbackData is the callback data parsed from the packet. type CallbackData struct { // CallbackAddress is the address of the callback actor. diff --git a/modules/apps/callbacks/types/callbacks_test.go b/modules/apps/callbacks/types/callbacks_test.go index d8ac06d5c46..2d8a9f295ba 100644 --- a/modules/apps/callbacks/types/callbacks_test.go +++ b/modules/apps/callbacks/types/callbacks_test.go @@ -465,7 +465,7 @@ func (s *CallbacksTypesTestSuite) TestGetDestSourceCallbackDataTransfer() { transferStack, ok := s.chainA.App.GetIBCKeeper().PortKeeper.Route(transfertypes.ModuleName) s.Require().True(ok) - packetUnmarshaler, ok := transferStack.(types.CallbacksCompatibleModule) + packetUnmarshaler, ok := transferStack.(porttypes.PacketUnmarshalarModule) s.Require().True(ok) s.path.Setup() diff --git a/modules/apps/callbacks/v2/ibc_middleware.go b/modules/apps/callbacks/v2/ibc_middleware.go index f15b8230890..d3a892962e2 100644 --- a/modules/apps/callbacks/v2/ibc_middleware.go +++ b/modules/apps/callbacks/v2/ibc_middleware.go @@ -42,7 +42,7 @@ func (rack RecvAcknowledgement) Acknowledgement() []byte { // IBCMiddleware implements the IBC v2 middleware interface // with the underlying application. type IBCMiddleware struct { - app types.CallbacksCompatibleModuleV2 + app api.PacketUnmarshalarModuleV2 writeAckWrapper api.WriteAcknowledgementWrapper contractKeeper types.ContractKeeper @@ -61,9 +61,9 @@ func NewIBCMiddleware( app api.IBCModule, writeAckWrapper api.WriteAcknowledgementWrapper, contractKeeper types.ContractKeeper, chanKeeperV2 types.ChannelKeeperV2, maxCallbackGas uint64, ) IBCMiddleware { - packetDataUnmarshalerApp, ok := app.(types.CallbacksCompatibleModuleV2) + packetDataUnmarshalerApp, ok := app.(api.PacketUnmarshalarModuleV2) if !ok { - panic(fmt.Errorf("underlying application does not implement %T", (*types.CallbacksCompatibleModule)(nil))) + panic(fmt.Errorf("underlying application does not implement %T", (*api.PacketUnmarshalarModuleV2)(nil))) } if contractKeeper == nil { diff --git a/modules/apps/callbacks/v2/ibc_middleware_test.go b/modules/apps/callbacks/v2/ibc_middleware_test.go index 417e4052f79..1f3d97ea36e 100644 --- a/modules/apps/callbacks/v2/ibc_middleware_test.go +++ b/modules/apps/callbacks/v2/ibc_middleware_test.go @@ -49,7 +49,7 @@ func (s *CallbacksTestSuite) TestNewIBCMiddleware() { func() { _ = v2.NewIBCMiddleware(nil, &channelkeeperv2.Keeper{}, simapp.ContractKeeper{}, &channelkeeperv2.Keeper{}, maxCallbackGas) }, - fmt.Errorf("underlying application does not implement %T", (*types.CallbacksCompatibleModule)(nil)), + fmt.Errorf("underlying application does not implement %T", (*api.PacketUnmarshalarModuleV2)(nil)), }, { "panics with nil contract keeper", diff --git a/modules/apps/packet-forward-middleware/README.md b/modules/apps/packet-forward-middleware/README.md new file mode 100644 index 00000000000..611036b4102 --- /dev/null +++ b/modules/apps/packet-forward-middleware/README.md @@ -0,0 +1,204 @@ +# packet-forward-middleware + +Middleware for forwarding IBC packets. + +Asynchronous acknowledgements are utilized for atomic multi-hop packet flows. The acknowledgement will only be written on the chain where the user initiated the packet flow after the forward/multi-hop sequence has completed (success or failure). This means that a user (i.e. an IBC application) only needs to monitor the chain where the initial transfer was sent for the response of the entire process. + +## About + +The packet-forward-middleware is an IBC middleware module built for Cosmos blockchains utilizing the IBC protocol. A chain which incorporates the +packet-forward-middleware is able to route incoming IBC packets from a source chain to a destination chain. As the Cosmos SDK/IBC become commonplace in the +blockchain space more and more zones will come online, these new zones joining are noticing a problem: they need to maintain a large amount of infrastructure +(archive nodes and relayers for each counterparty chain) to connect with all the chains in the ecosystem, a number that is continuing to increase quickly. Luckily +this problem has been anticipated and IBC has been architected to accommodate multi-hop transactions. However, a packet forwarding/routing feature was not in the +initial IBC release. + +## Sequence diagrams + +### Let's stipulate the following connections between chains A, B, C, and D + +```mermaid +flowchart LR + A((Chain A)) + B((Chain B)) + C((Chain C)) + D((Chain D)) + + A <--"ch-0 ch-1 (IBC)"--> B + B <--"ch-2 ch-3 (IBC)"--> C + C <--"ch-4 ch-5 (IBC)"--> D +``` + +### SCENARIO: Via PFM, Chain A wants to pass a message to Chain D (to which it's not directly connected) + +```mermaid +sequenceDiagram + autonumber + Chain A ->> Chain B: PFM transfer + Chain B --> Chain B: recv_packet + Chain B ->> Chain C: forward + Chain C --> Chain C: recv_packet + Chain C ->> Chain D: forward + Chain D --> Chain D: recv_packet + Chain D ->> Chain C: ack + Chain C ->> Chain B: ack + Chain B ->> Chain A: ack +``` + +### SCENARIO: Multi-hop A->B->C->D, C->D `recv_packet` error, refund back to A + +```mermaid +sequenceDiagram + autonumber + Chain A ->> Chain B: PFM transfer + Chain B --> Chain B: recv_packet + Chain B ->> Chain C: forward + Chain C --> Chain C: recv_packet + Chain C ->> Chain D: forward + Chain D --> Chain D: ☠️ recv_packet ERR ☠️ + Chain D ->> Chain C: ☠️ ack ERR ☠️ + Chain C ->> Chain B: ☠️ ack ERR ☠️ + Chain B ->> Chain A: ☠️ ack ERR ☠️ +``` + +### SCENARIO: Forward A->B->C with 1 retry, max timeouts occurs, refund back to A + +```mermaid +sequenceDiagram + autonumber + Chain A ->> Chain B: PFM transfer + Chain B --> Chain B: recv_packet + Chain B ->> Chain C: forward + Chain C --x Chain B: timeout + Chain B ->> Chain C: forward retry + Chain C --x Chain B: timeout + Chain B ->> Chain A: ☠️ ack ERR ☠️ +``` + +## Examples + +Utilizing the packet `memo` field, instructions can be encoded as JSON for multi-hop sequences. + +### Minimal Example - Chain forward A->B->C + +- The packet-forward-middleware integrated on Chain B. +- The packet data `receiver` for the `MsgTransfer` on Chain A is set to `"pfm"` or some other invalid bech32 string.* +- The packet `memo` is included in `MsgTransfer` by user on Chain A. + +memo: + +```json +{ + "forward": { + "receiver": "chain-c-bech32-address", + "port": "transfer", + "channel": "channel-123" + } +} +``` + +### Full Example - Chain forward A->B->C->D with retry on timeout + +- The packet-forward-middleware integrated on Chain B and Chain C. +- The packet data `receiver` for the `MsgTransfer` on Chain A is set to `"pfm"` or some other invalid bech32 string.* +- The forward metadata `receiver` for the hop from Chain B to Chain C is set to `"pfm"` or some other invalid bech32 string.* +- The packet `memo` is included in `MsgTransfer` by user on Chain A. +- A packet timeout of 10 minutes and 2 retries is set for both forwards. + +In the case of a timeout after 10 minutes for either forward, the packet would be retried up to 2 times, at which case an error ack would be written to issue a refund on the prior chain. + +`next` is the `memo` to pass for the next transfer hop. Per `memo` intended usage of a JSON string, it should be either JSON which will be Marshaled retaining key order, or an escaped JSON string which will be passed directly. + +`next` as JSON + +```json +{ + "forward": { + "receiver": "pfm", // purposely using invalid bech32 here* + "port": "transfer", + "channel": "channel-123", + "timeout": "10m", + "retries": 2, + "next": { + "forward": { + "receiver": "chain-d-bech32-address", + "port": "transfer", + "channel":"channel-234", + "timeout":"10m", + "retries": 2 + } + } + } +} +``` + +`next` as escaped JSON string + +```json +{ + "forward": { + "receiver": "pfm", // purposely using invalid bech32 here* + "port": "transfer", + "channel": "channel-123", + "timeout": "10m", + "retries": 2, + "next": "{\"forward\":{\"receiver\":\"chain-d-bech32-address\",\"port\":\"transfer\",\"channel\":\"channel-234\",\"timeout\":\"10m\",\"retries\":2}}" + } +} +``` + +## Intermediate Receivers* + +PFM does not need the packet data `receiver` address to be valid, as it will create a hash of the sender and channel to derive a receiver address on the intermediate chains. This is done for security purposes to ensure that users cannot move funds through arbitrary accounts on intermediate chains. + +To prevent accidentally sending funds to a chain which does not have PFM, it is recommended to use an invalid bech32 string (such as `"pfm"`) for the `receiver` on intermediate chains. By using an invalid bech32 string, a transfer that is accidentally sent to a chain that does not have PFM would fail to be received, and properly refunded to the user on the source chain, rather than having funds get stuck on the intermediate chain. + +The examples above show the intended usage of the `receiver` field for one or multiple intermediate PFM chains. + +## Implementation details + +Flow sequence mainly encoded in [middleware](packetforward/ibc_middleware.go) and in [keeper](packetforward/keeper/keeper.go). + +Describes `A` sending to `C` via `B` in several scenarios with operational opened channels, enabled denom composition, fees and available to refund, but no retries. + +Generally without `memo` to handle, all handling by this module is delegated to ICS-020. ICS-020 ACK are written and parsed in any case (ACK are backwarded). + +### A -> B -> C full success + +1. `A` This sends packet over underlying ICS-004 wrapper with memo as is. +2. `B` This receives packet and parses it into ICS-020 packet. +3. `B` Validates `forward` packet on this step, return `ACK` error if fails. +4. `B` If other middleware not yet called ICS-020, call it and ACK error on fail. Tokens minted or unescrowed here. +5. `B` Handle denom. If denom prefix is from `B`, remove it. If denom prefix is other chain - add `B` prefix. +6. `B` Take fee, create new ICS-004 packet with timeout from forward for next step, and remaining inner `memo`. +7. `B` Send transfer to `C` with parameters obtained from `memo`. Tokens burnt or escrowed here. +8. `B` Store tracking `in flight packet` under next `(channel, port, ICS-20 transfer sequence)`, do not `ACK` packet yet. +9. `C` Handle ICS-020 packet as usual. +10. `B` On ICS-020 ACK from `C` find `in flight packet`, delete it and write `ACK` for original packet from `A`. +11. `A` Handle ICS-020 `ACK` as usual + +[Example](https://mintscan.io/osmosis-testnet/txs/FAB912347B8729FFCA92AC35E6B1E83BC8169DE7CC2C254A5A3F70C8EC35D771?height=3788973) of USDC transfer from Osmosis -> Noble -> Sei + +### A -> B -> C with C error ACK + +10. `B` On ICS-020 ACK from `C` find `in flight packet`, delete it +11. `B` Burns or escrows tokens. +12. `B` And write error `ACK` for original packet from `A`. +13. `A` Handle ICS-020 timeout as usual +14. `C` writes success `ACK` for packet from `B` + +Same behavior in case of timeout on `C` + +### A packet timeouts on B before C timeouts packet from B + +10. `A` Cannot timeout because `in flight packet` has proof on `B` of packet inclusion. +11. `B` waits for ACK or timeout from `C`. +12. `B` timeout from `C` becomes fail `ACK` on `B` for `A` +13. `A` receives success or fail `ACK`, but not timeout + +In this case `A` assets `hang` until final hop timeouts or ACK. + +## References + +- +- PFM was originally implemented in diff --git a/modules/apps/packet-forward-middleware/ibc_middleware.go b/modules/apps/packet-forward-middleware/ibc_middleware.go new file mode 100644 index 00000000000..08e9847d326 --- /dev/null +++ b/modules/apps/packet-forward-middleware/ibc_middleware.go @@ -0,0 +1,371 @@ +package packetforward + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/hashicorp/go-metrics" + + errorsmod "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/keeper" + "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/types" + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" + porttypes "github.com/cosmos/ibc-go/v10/modules/core/05-port/types" + ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" +) + +var ( + _ porttypes.Middleware = &IBCMiddleware{} + _ porttypes.PacketUnmarshalarModule = &IBCMiddleware{} +) + +// IBCMiddleware implements the ICS26 callbacks for the forward middleware given the +// forward keeper and the underlying application. +type IBCMiddleware struct { + app porttypes.PacketUnmarshalarModule + keeper *keeper.Keeper + + retriesOnTimeout uint8 + forwardTimeout time.Duration +} + +// NewIBCMiddleware creates a new IBCMiddleware given the keeper and underlying application. +func NewIBCMiddleware(app porttypes.PacketUnmarshalarModule, k *keeper.Keeper, retriesOnTimeout uint8, forwardTimeout time.Duration) IBCMiddleware { + return IBCMiddleware{ + app: app, + keeper: k, + retriesOnTimeout: retriesOnTimeout, + forwardTimeout: forwardTimeout, + } +} + +// OnChanOpenInit implements the IBCModule interface. +func (im IBCMiddleware) OnChanOpenInit(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID string, channelID string, counterparty channeltypes.Counterparty, version string) (string, error) { + return im.app.OnChanOpenInit(ctx, order, connectionHops, portID, channelID, counterparty, version) +} + +// OnChanOpenTry implements the IBCModule interface. +func (im IBCMiddleware) OnChanOpenTry(ctx sdk.Context, order channeltypes.Order, connectionHops []string, portID, channelID string, counterparty channeltypes.Counterparty, counterpartyVersion string) (version string, err error) { + return im.app.OnChanOpenTry(ctx, order, connectionHops, portID, channelID, counterparty, counterpartyVersion) +} + +// OnChanOpenAck implements the IBCModule interface. +func (im IBCMiddleware) OnChanOpenAck(ctx sdk.Context, portID, channelID string, counterpartyChannelID string, counterpartyVersion string) error { + return im.app.OnChanOpenAck(ctx, portID, channelID, counterpartyChannelID, counterpartyVersion) +} + +// OnChanOpenConfirm implements the IBCModule interface. +func (im IBCMiddleware) OnChanOpenConfirm(ctx sdk.Context, portID, channelID string) error { + return im.app.OnChanOpenConfirm(ctx, portID, channelID) +} + +// OnChanCloseInit implements the IBCModule interface. +func (im IBCMiddleware) OnChanCloseInit(ctx sdk.Context, portID, channelID string) error { + return im.app.OnChanCloseInit(ctx, portID, channelID) +} + +// OnChanCloseConfirm implements the IBCModule interface. +func (im IBCMiddleware) OnChanCloseConfirm(ctx sdk.Context, portID, channelID string) error { + return im.app.OnChanCloseConfirm(ctx, portID, channelID) +} + +// UnmarshalPacketData implements PacketDataUnmarshaler. +func (im IBCMiddleware) UnmarshalPacketData(ctx sdk.Context, portID string, channelID string, bz []byte) (any, string, error) { + return im.app.UnmarshalPacketData(ctx, portID, channelID, bz) +} + +func getDenomForThisChain(port, channel, counterpartyPort, counterpartyChannel, denomPath string) string { + denom := transfertypes.ExtractDenomFromPath(denomPath) + + if denom.HasPrefix(counterpartyPort, counterpartyChannel) { + + // unwind denom + denom.Trace = denom.Trace[1:] + if len(denom.Trace) == 0 { + // denom is now unwound back to native denom + return denom.Path() + } + // denom is still IBC denom + return denom.IBCDenom() + } + // append port and channel from this chain to denom + trace := []transfertypes.Hop{transfertypes.NewHop(port, channel)} + denom.Trace = append(trace, denom.Trace...) + + return denom.IBCDenom() +} + +// getBoolFromAny returns the bool value is any is a valid bool, otherwise false. +func getBoolFromAny(value any) bool { + if value == nil { + return false + } + boolVal, ok := value.(bool) + if !ok { + return false + } + return boolVal +} + +// GetReceiver returns the receiver address for a given channel and original sender. +// it overrides the receiver address to be a hash of the channel/origSender so that +// the receiver address is deterministic and can be used to identify the sender on the +// initial chain. +func GetReceiver(channel string, originalSender string) (string, error) { + senderStr := fmt.Sprintf("%s/%s", channel, originalSender) + senderHash32 := address.Hash(types.ModuleName, []byte(senderStr)) + sender := sdk.AccAddress(senderHash32[:20]) + bech32Prefix := sdk.GetConfig().GetBech32AccountAddrPrefix() + return sdk.Bech32ifyAddressBytes(bech32Prefix, sender) +} + +// newErrorAcknowledgement returns an error that identifies PFM and provides the error. +// It's okay if these errors are non-deterministic, because they will not be committed to state, only emitted as events. +func newErrorAcknowledgement(err error) channeltypes.Acknowledgement { + return channeltypes.Acknowledgement{ + Response: &channeltypes.Acknowledgement_Error{ + Error: fmt.Sprintf("packet-forward-middleware error: %s", err.Error()), + }, + } +} + +// OnRecvPacket checks the memo field on this packet and if the metadata inside's root key indicates this packet +// should be handled by the swap middleware it attempts to perform a swap. If the swap is successful +// the underlying application's OnRecvPacket callback is invoked, an ack error is returned otherwise. +func (im IBCMiddleware) OnRecvPacket(ctx sdk.Context, channelVersion string, packet channeltypes.Packet, relayer sdk.AccAddress) ibcexported.Acknowledgement { + logger := im.keeper.Logger(ctx) + + var data transfertypes.FungibleTokenPacketData + if err := transfertypes.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil { + logger.Debug(fmt.Sprintf("packetForwardMiddleware OnRecvPacket payload is not a FungibleTokenPacketData: %s", err.Error())) + return im.app.OnRecvPacket(ctx, channelVersion, packet, relayer) + } + + logger.Debug("packetForwardMiddleware OnRecvPacket", + "sequence", packet.Sequence, + "src-channel", packet.SourceChannel, + "src-port", packet.SourcePort, + "dst-channel", packet.DestinationChannel, + "dst-port", packet.DestinationPort, + "amount", data.Amount, + "denom", data.Denom, + "memo", data.Memo, + ) + + d := make(map[string]any) + err := json.Unmarshal([]byte(data.Memo), &d) + logger.Debug("packetForwardMiddleware json", "memo", data.Memo) + if err != nil || d["forward"] == nil { + // not a packet that should be forwarded + logger.Debug("packetForwardMiddleware OnRecvPacket forward metadata does not exist") + return im.app.OnRecvPacket(ctx, channelVersion, packet, relayer) + } + m := &types.PacketMetadata{} + err = json.Unmarshal([]byte(data.Memo), m) + if err != nil { + logger.Error("packetForwardMiddleware OnRecvPacket error parsing forward metadata", "error", err) + return newErrorAcknowledgement(fmt.Errorf("error parsing forward metadata: %w", err)) + } + + metadata := m.Forward + + goCtx := ctx.Context() + nonrefundable := getBoolFromAny(goCtx.Value(types.NonrefundableKey{})) + + if err := metadata.Validate(); err != nil { + logger.Error("packetForwardMiddleware OnRecvPacket forward metadata is invalid", "error", err) + return newErrorAcknowledgement(err) + } + + // override the receiver so that senders cannot move funds through arbitrary addresses. + overrideReceiver, err := GetReceiver(packet.DestinationChannel, data.Sender) + if err != nil { + logger.Error("packetForwardMiddleware OnRecvPacket failed to construct override receiver", "error", err) + return newErrorAcknowledgement(fmt.Errorf("failed to construct override receiver: %w", err)) + } + + if err := im.receiveFunds(ctx, channelVersion, packet, data, overrideReceiver, relayer); err != nil { + logger.Error("packetForwardMiddleware OnRecvPacket error receiving packet", "error", err) + return newErrorAcknowledgement(fmt.Errorf("error receiving packet: %w", err)) + } + + // if this packet's token denom is already the base denom for some native token on this chain, + // we do not need to do any further composition of the denom before forwarding the packet + denomOnThisChain := getDenomForThisChain(packet.DestinationPort, packet.DestinationChannel, packet.SourcePort, packet.SourceChannel, data.Denom) + + amountInt, ok := sdkmath.NewIntFromString(data.Amount) + if !ok { + logger.Error("packetForwardMiddleware OnRecvPacket error parsing amount for forward", "amount", data.Amount) + return newErrorAcknowledgement(fmt.Errorf("error parsing amount for forward: %s", data.Amount)) + } + + token := sdk.NewCoin(denomOnThisChain, amountInt) + + timeout := time.Duration(metadata.Timeout) + + if timeout.Nanoseconds() <= 0 { + timeout = im.forwardTimeout + } + + var retries uint8 + if metadata.Retries != nil { + retries = *metadata.Retries + } else { + retries = im.retriesOnTimeout + } + + err = im.keeper.ForwardTransferPacket(ctx, nil, packet, data.Sender, overrideReceiver, metadata, token, retries, timeout, []metrics.Label{}, nonrefundable) + if err != nil { + logger.Error("packetForwardMiddleware OnRecvPacket error forwarding packet", "error", err) + return newErrorAcknowledgement(err) + } + + // returning nil ack will prevent WriteAcknowledgement from occurring for forwarded packet. + // This is intentional so that the acknowledgement will be written later based on the ack/timeout of the forwarded packet. + return nil +} + +// receiveFunds receives funds from the packet into the override receiver +// address and returns an error if the funds cannot be received. +func (im IBCMiddleware) receiveFunds(ctx sdk.Context, channelVersion string, packet channeltypes.Packet, data transfertypes.FungibleTokenPacketData, overrideReceiver string, relayer sdk.AccAddress) error { + overrideData := transfertypes.FungibleTokenPacketData{ + Denom: data.Denom, + Amount: data.Amount, + Sender: data.Sender, + Receiver: overrideReceiver, // override receiver + // Memo explicitly zeroed + } + overrideDataBz := transfertypes.ModuleCdc.MustMarshalJSON(&overrideData) + overridePacket := channeltypes.Packet{ + Sequence: packet.Sequence, + SourcePort: packet.SourcePort, + SourceChannel: packet.SourceChannel, + DestinationPort: packet.DestinationPort, + DestinationChannel: packet.DestinationChannel, + Data: overrideDataBz, // override data + TimeoutHeight: packet.TimeoutHeight, + TimeoutTimestamp: packet.TimeoutTimestamp, + } + + ack := im.app.OnRecvPacket(ctx, channelVersion, overridePacket, relayer) + if ack == nil { + return errors.New("ack is nil") + } + + if !ack.Success() { + return fmt.Errorf("ack error: %s", string(ack.Acknowledgement())) + } + + return nil +} + +// OnAcknowledgementPacket implements the IBCModule interface. +func (im IBCMiddleware) OnAcknowledgementPacket(ctx sdk.Context, channelVersion string, packet channeltypes.Packet, acknowledgement []byte, relayer sdk.AccAddress) error { + var data transfertypes.FungibleTokenPacketData + if err := transfertypes.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil { + im.keeper.Logger(ctx).Error("packetForwardMiddleware error parsing packet data from ack packet", + "sequence", packet.Sequence, + "src-channel", packet.SourceChannel, + "src-port", packet.SourcePort, + "dst-channel", packet.DestinationChannel, + "dst-port", packet.DestinationPort, + "error", err, + ) + return im.app.OnAcknowledgementPacket(ctx, channelVersion, packet, acknowledgement, relayer) + } + + im.keeper.Logger(ctx).Debug("packetForwardMiddleware OnAcknowledgementPacket", + "sequence", packet.Sequence, + "src-channel", packet.SourceChannel, + "src-port", packet.SourcePort, + "dst-channel", packet.DestinationChannel, + "dst-port", packet.DestinationPort, + "amount", data.Amount, + "denom", data.Denom, + ) + + var ack channeltypes.Acknowledgement + if err := channeltypes.SubModuleCdc.UnmarshalJSON(acknowledgement, &ack); err != nil { + return errorsmod.Wrapf(sdkerrors.ErrUnknownRequest, "cannot unmarshal ICS-20 transfer packet acknowledgement: %v", err) + } + + inFlightPacket, err := im.keeper.GetInflightPacket(ctx, packet) + if err != nil { + return err + } + + if inFlightPacket != nil { + im.keeper.RemoveInFlightPacket(ctx, packet) + // this is a forwarded packet, so override handling to avoid refund from being processed. + return im.keeper.WriteAcknowledgementForForwardedPacket(ctx, packet, data, inFlightPacket, ack) + } + + return im.app.OnAcknowledgementPacket(ctx, channelVersion, packet, acknowledgement, relayer) +} + +// OnTimeoutPacket implements the IBCModule interface. +func (im IBCMiddleware) OnTimeoutPacket(ctx sdk.Context, channelVersion string, packet channeltypes.Packet, relayer sdk.AccAddress) error { + var data transfertypes.FungibleTokenPacketData + if err := transfertypes.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil { + im.keeper.Logger(ctx).Error("packetForwardMiddleware error parsing packet data from timeout packet", + "sequence", packet.Sequence, + "src-channel", packet.SourceChannel, + "src-port", packet.SourcePort, + "dst-channel", packet.DestinationChannel, + "dst-port", packet.DestinationPort, + "error", err, + ) + return im.app.OnTimeoutPacket(ctx, channelVersion, packet, relayer) + } + + im.keeper.Logger(ctx).Debug("packetForwardMiddleware OnTimeoutPacket", + "sequence", packet.Sequence, + "src-channel", packet.SourceChannel, + "src-port", packet.SourcePort, + "dst-channel", packet.DestinationChannel, + "dst-port", packet.DestinationPort, + "amount", data.Amount, + "denom", data.Denom, + ) + + inFlightPacket, err := im.keeper.TimeoutShouldRetry(ctx, packet) + if inFlightPacket != nil { + im.keeper.RemoveInFlightPacket(ctx, packet) + if err != nil { + // this is a forwarded packet, so override handling to avoid refund from being processed on this chain. + // WriteAcknowledgement with proxied ack to return success/fail to previous chain. + return im.keeper.WriteAcknowledgementForForwardedPacket(ctx, packet, data, inFlightPacket, newErrorAcknowledgement(err)) + } + // timeout should be retried. In order to do that, we need to handle this timeout to refund on this chain first. + if err := im.app.OnTimeoutPacket(ctx, channelVersion, packet, relayer); err != nil { + return err + } + return im.keeper.RetryTimeout(ctx, packet.SourceChannel, packet.SourcePort, data, inFlightPacket) + } + + return im.app.OnTimeoutPacket(ctx, channelVersion, packet, relayer) +} + +// SendPacket implements the ICS4 Wrapper interface. +func (im IBCMiddleware) SendPacket(ctx sdk.Context, sourcePort, sourceChannel string, timeoutHeight clienttypes.Height, timeoutTimestamp uint64, data []byte) (sequence uint64, err error) { + return im.keeper.SendPacket(ctx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) +} + +// WriteAcknowledgement implements the ICS4 Wrapper interface. +func (im IBCMiddleware) WriteAcknowledgement(ctx sdk.Context, packet ibcexported.PacketI, ack ibcexported.Acknowledgement) error { + return im.keeper.WriteAcknowledgement(ctx, packet, ack) +} + +func (im IBCMiddleware) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { + return im.keeper.GetAppVersion(ctx, portID, channelID) +} diff --git a/modules/apps/packet-forward-middleware/ibc_middleware_test.go b/modules/apps/packet-forward-middleware/ibc_middleware_test.go new file mode 100644 index 00000000000..777ffb88ed3 --- /dev/null +++ b/modules/apps/packet-forward-middleware/ibc_middleware_test.go @@ -0,0 +1,249 @@ +package packetforward_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/suite" + + packetforward "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware" + packetforwardkeeper "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/keeper" + packetforwardtypes "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/types" + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" + porttypes "github.com/cosmos/ibc-go/v10/modules/core/05-port/types" + ibctesting "github.com/cosmos/ibc-go/v10/testing" +) + +type PFMTestSuite struct { + suite.Suite + + coordinator *ibctesting.Coordinator + + chainA *ibctesting.TestChain + chainB *ibctesting.TestChain + chainC *ibctesting.TestChain + + pathAB *ibctesting.Path + pathBC *ibctesting.Path +} + +func TestPFMTestSuite(t *testing.T) { + suite.Run(t, new(PFMTestSuite)) +} + +// setupChains sets up a coordinator with 3 test chains. +func (s *PFMTestSuite) 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.pathAB = ibctesting.NewTransferPath(s.chainA, s.chainB) + s.pathAB.Setup() + + s.pathBC = ibctesting.NewTransferPath(s.chainB, s.chainC) + s.pathBC.Setup() +} + +func (s *PFMTestSuite) TestOnRecvPacket_NonfungibleToken() { + s.setupChains() + + ctx := s.chainA.GetContext() + version := s.pathAB.EndpointA.GetChannel().Version + relayerAddr := s.chainA.SenderAccount.GetAddress() + + pfm := s.pktForwardMiddleware(s.chainA) + ack := pfm.OnRecvPacket(ctx, version, channeltypes.Packet{}, relayerAddr) + s.Require().False(ack.Success()) + + expectedAck := &channeltypes.Acknowledgement{} + err := s.chainA.Codec.UnmarshalJSON(ack.Acknowledgement(), expectedAck) + s.Require().NoError(err) + + // Transfer keeper returns this error if the packet received is not a fungible token. + s.Require().Equal("ABCI code: 12: error handling packet: see events for details", expectedAck.GetError()) +} + +func (s *PFMTestSuite) TestOnRecvPacket_NoMemo() { + s.setupChains() + + ctx := s.chainA.GetContext() + version := s.pathAB.EndpointA.GetChannel().Version + relayerAddr := s.chainA.SenderAccount.GetAddress() + receiverAddr := s.chainB.SenderAccount.GetAddress() + + packet := s.transferPacket(relayerAddr.String(), receiverAddr.String(), s.pathAB, 0, "{}") + + pfm := s.pktForwardMiddleware(s.chainA) + ack := pfm.OnRecvPacket(ctx, version, packet, relayerAddr) + s.Require().True(ack.Success()) + + expectedAck := &channeltypes.Acknowledgement{} + err := s.chainA.Codec.UnmarshalJSON(ack.Acknowledgement(), expectedAck) + s.Require().NoError(err) + + s.Require().Equal("", expectedAck.GetError()) + s.Require().ElementsMatch([]byte{1}, expectedAck.GetResult()) +} + +func (s *PFMTestSuite) TestOnRecvPacket_InvalidReceiver() { + s.setupChains() + + ctx := s.chainA.GetContext() + version := s.pathAB.EndpointA.GetChannel().Version + relayerAddr := s.chainA.SenderAccount.GetAddress() + + packet := s.transferPacket(relayerAddr.String(), "", s.pathAB, 0, nil) + + pfm := s.pktForwardMiddleware(s.chainA) + ack := pfm.OnRecvPacket(ctx, version, packet, relayerAddr) + s.Require().False(ack.Success()) + + expectedAck := &channeltypes.Acknowledgement{} + err := s.chainA.Codec.UnmarshalJSON(ack.Acknowledgement(), expectedAck) + s.Require().NoError(err) + + s.Require().Equal("ABCI code: 5: error handling packet: see events for details", expectedAck.GetError()) + s.Require().Empty(expectedAck.GetResult()) +} + +func (s *PFMTestSuite) TestOnRecvPacket_NoForward() { + s.setupChains() + + ctx := s.chainA.GetContext() + version := s.pathAB.EndpointA.GetChannel().Version + + senderAddr := s.chainA.SenderAccount.GetAddress() + receiverAddr := s.chainB.SenderAccount.GetAddress() + + packet := s.transferPacket(senderAddr.String(), receiverAddr.String(), s.pathAB, 0, nil) + + pfm := s.pktForwardMiddleware(s.chainA) + ack := pfm.OnRecvPacket(ctx, version, packet, senderAddr) + s.Require().True(ack.Success()) + + expectedAck := &channeltypes.Acknowledgement{} + err := s.chainA.Codec.UnmarshalJSON(ack.Acknowledgement(), expectedAck) + s.Require().NoError(err) + s.Require().Equal("", expectedAck.GetError()) + + s.Require().Equal([]byte{1}, expectedAck.GetResult()) +} + +func (s *PFMTestSuite) TestOnRecvPacket_RecvPacketFailed() { + s.setupChains() + + transferKeeper := s.chainA.GetSimApp().TransferKeeper + ctx := s.chainA.GetContext() + transferKeeper.SetParams(ctx, transfertypes.Params{ReceiveEnabled: false}) + + version := s.pathAB.EndpointA.GetChannel().Version + + senderAddr := s.chainA.SenderAccount.GetAddress() + receiverAddr := s.chainB.SenderAccount.GetAddress() + metadata := &packetforwardtypes.PacketMetadata{ + Forward: &packetforwardtypes.ForwardMetadata{ + Receiver: receiverAddr.String(), + Port: s.pathAB.EndpointA.ChannelConfig.PortID, + Channel: s.pathAB.EndpointA.ChannelID, + }, + } + packet := s.transferPacket(senderAddr.String(), receiverAddr.String(), s.pathAB, 0, metadata) + + pfm := s.pktForwardMiddleware(s.chainA) + ack := pfm.OnRecvPacket(ctx, version, packet, senderAddr) + s.Require().False(ack.Success()) + + expectedAck := &channeltypes.Acknowledgement{} + + err := s.chainA.Codec.UnmarshalJSON(ack.Acknowledgement(), expectedAck) + s.Require().NoError(err) + s.Require().Equal("packet-forward-middleware error: error receiving packet: ack error: {\"error\":\"ABCI code: 8: error handling packet: see events for details\"}", expectedAck.GetError()) + + s.Require().Equal([]byte(nil), expectedAck.GetResult()) +} + +func (s *PFMTestSuite) TestOnRecvPacket_ForwardNoFee() { + s.setupChains() + + senderAddr := s.chainA.SenderAccount.GetAddress() + receiverAddr := s.chainC.SenderAccount.GetAddress() + metadata := &packetforwardtypes.PacketMetadata{ + Forward: &packetforwardtypes.ForwardMetadata{ + Receiver: receiverAddr.String(), + Port: s.pathBC.EndpointA.ChannelConfig.PortID, + Channel: s.pathBC.EndpointA.ChannelID, + }, + } + packet := s.transferPacket(senderAddr.String(), receiverAddr.String(), s.pathAB, 0, metadata) + version := s.pathAB.EndpointA.GetChannel().Version + ctxB := s.chainB.GetContext() + + pfmB := s.pktForwardMiddleware(s.chainB) + ack := pfmB.OnRecvPacket(ctxB, version, packet, senderAddr) + s.Require().Nil(ack) + + // Check that chain C has received the packet + ctxC := s.chainC.GetContext() + packet = s.transferPacket(senderAddr.String(), receiverAddr.String(), s.pathBC, 0, nil) + version = s.pathBC.EndpointA.GetChannel().Version + + pfmC := s.pktForwardMiddleware(s.chainC) + ack = pfmC.OnRecvPacket(ctxC, version, packet, senderAddr) + s.Require().NotNil(ack) + + // Ack on chainC + packet = s.transferPacket(senderAddr.String(), receiverAddr.String(), s.pathBC, 1, nil) + err := pfmC.OnAcknowledgementPacket(ctxC, version, packet, ack.Acknowledgement(), senderAddr) + s.Require().NoError(err) + + // Ack on ChainB + err = pfmB.OnAcknowledgementPacket(ctxB, version, packet, ack.Acknowledgement(), senderAddr) + s.Require().NoError(err) +} + +func (s *PFMTestSuite) pktForwardMiddleware(chain *ibctesting.TestChain) packetforward.IBCMiddleware { + pfmKeeper := chain.GetSimApp().PFMKeeper + + ibcModule, ok := chain.App.GetIBCKeeper().PortKeeper.Route(transfertypes.ModuleName) + s.Require().True(ok) + + transferStack, ok := ibcModule.(porttypes.PacketUnmarshalarModule) + s.Require().True(ok) + + ibcMiddleware := packetforward.NewIBCMiddleware(transferStack, pfmKeeper, 0, packetforwardkeeper.DefaultForwardTransferPacketTimeoutTimestamp) + return ibcMiddleware +} + +func (s *PFMTestSuite) transferPacket(sender string, receiver string, path *ibctesting.Path, seq uint64, metadata any) channeltypes.Packet { + s.T().Helper() + tokenPacket := transfertypes.FungibleTokenPacketData{ + Denom: "uatom", + Amount: "100", + Sender: sender, + Receiver: receiver, + } + + if metadata != nil { + if mStr, ok := metadata.(string); ok { + tokenPacket.Memo = mStr + } else { + memo, err := json.Marshal(metadata) + s.Require().NoError(err) + tokenPacket.Memo = string(memo) + } + } + + tokenData, err := transfertypes.ModuleCdc.MarshalJSON(&tokenPacket) + s.Require().NoError(err) + + return channeltypes.Packet{ + SourcePort: path.EndpointA.ChannelConfig.PortID, + SourceChannel: path.EndpointA.ChannelID, + DestinationPort: path.EndpointB.ChannelConfig.PortID, + DestinationChannel: path.EndpointB.ChannelID, + Data: tokenData, + Sequence: seq, + } +} diff --git a/modules/apps/packet-forward-middleware/keeper/genesis.go b/modules/apps/packet-forward-middleware/keeper/genesis.go new file mode 100644 index 00000000000..9b230a5e8f0 --- /dev/null +++ b/modules/apps/packet-forward-middleware/keeper/genesis.go @@ -0,0 +1,41 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/types" +) + +// TODO: Write unit tests #8321 + +// InitGenesis +func (k *Keeper) InitGenesis(ctx sdk.Context, state types.GenesisState) { + // Initialize store refund path for forwarded packets in genesis state that have not yet been acked. + store := k.storeService.OpenKVStore(ctx) + for key, value := range state.InFlightPackets { + key := key + value := value + bz := k.cdc.MustMarshal(&value) + if err := store.Set([]byte(key), bz); err != nil { + panic(err) + } + } +} + +// ExportGenesis +func (k *Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { + store := k.storeService.OpenKVStore(ctx) + + inFlightPackets := make(map[string]types.InFlightPacket) + + itr, err := store.Iterator(nil, nil) + if err != nil { + panic(err) + } + for ; itr.Valid(); itr.Next() { + var inFlightPacket types.InFlightPacket + k.cdc.MustUnmarshal(itr.Value(), &inFlightPacket) + inFlightPackets[string(itr.Key())] = inFlightPacket + } + return &types.GenesisState{InFlightPackets: inFlightPackets} +} diff --git a/modules/apps/packet-forward-middleware/keeper/keeper.go b/modules/apps/packet-forward-middleware/keeper/keeper.go new file mode 100644 index 00000000000..bc00e7b4fae --- /dev/null +++ b/modules/apps/packet-forward-middleware/keeper/keeper.go @@ -0,0 +1,448 @@ +package keeper + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/hashicorp/go-metrics" + + corestore "cosmossdk.io/core/store" + errorsmod "cosmossdk.io/errors" + "cosmossdk.io/log" + sdkmath "cosmossdk.io/math" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/telemetry" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/bech32" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/types" + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" + porttypes "github.com/cosmos/ibc-go/v10/modules/core/05-port/types" + ibcexported "github.com/cosmos/ibc-go/v10/modules/core/exported" + coremetrics "github.com/cosmos/ibc-go/v10/modules/core/metrics" +) + +var ( + // DefaultTransferPacketTimeoutHeight is the timeout height following IBC defaults + DefaultTransferPacketTimeoutHeight = clienttypes.NewHeight(0, 0) + + // DefaultForwardTransferPacketTimeoutTimestamp is the timeout timestamp following IBC defaults + DefaultForwardTransferPacketTimeoutTimestamp = time.Duration(10) * time.Minute +) + +// Keeper defines the packet forward middleware keeper +type Keeper struct { + storeService corestore.KVStoreService + cdc codec.BinaryCodec + + transferKeeper types.TransferKeeper + channelKeeper types.ChannelKeeper + bankKeeper types.BankKeeper + ics4Wrapper porttypes.ICS4Wrapper + + // the address capable of executing a MsgUpdateParams message. Typically, this + // should be the x/gov module account. + authority string +} + +// NewKeeper creates a new forward Keeper instance +func NewKeeper(cdc codec.BinaryCodec, storeService corestore.KVStoreService, transferKeeper types.TransferKeeper, channelKeeper types.ChannelKeeper, bankKeeper types.BankKeeper, ics4Wrapper porttypes.ICS4Wrapper, authority string) *Keeper { + return &Keeper{ + cdc: cdc, + storeService: storeService, + transferKeeper: transferKeeper, + channelKeeper: channelKeeper, + bankKeeper: bankKeeper, + ics4Wrapper: ics4Wrapper, + authority: authority, + } +} + +// GetAuthority returns the module's authority. +func (k *Keeper) GetAuthority() string { + return k.authority +} + +// SetTransferKeeper sets the transferKeeper +func (k *Keeper) SetTransferKeeper(transferKeeper types.TransferKeeper) { + k.transferKeeper = transferKeeper +} + +// Logger returns a module-specific logger. +func (*Keeper) Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", "x/"+ibcexported.ModuleName+"-"+types.ModuleName) +} + +// moveFundsToUserRecoverableAccount will move the funds from the escrow account to the user recoverable account +// this is only used when the maximum timeouts have been reached or there is an acknowledgement error and the packet is nonrefundable, +// i.e. an operation has occurred to make the original packet funds inaccessible to the user, e.g. a swap. +// We cannot refund the funds back to the original chain, so we move them to an account on this chain that the user can access. +func (k *Keeper) moveFundsToUserRecoverableAccount(ctx sdk.Context, packet channeltypes.Packet, data transfertypes.FungibleTokenPacketData, inFlightPacket *types.InFlightPacket) error { + fullDenomPath := data.Denom + + amount, ok := sdkmath.NewIntFromString(data.Amount) + if !ok { + return fmt.Errorf("failed to parse amount from packet data for forward recovery: %s", data.Amount) + } + denom := transfertypes.ExtractDenomFromPath(fullDenomPath) + coin := sdk.NewCoin(denom.IBCDenom(), amount) + + userAccount, err := userRecoverableAccount(inFlightPacket) + if err != nil { + return fmt.Errorf("failed to get user recoverable account: %w", err) + } + + if !denom.HasPrefix(packet.SourcePort, packet.SourceChannel) { + // mint vouchers back to sender + if err := k.bankKeeper.MintCoins(ctx, transfertypes.ModuleName, sdk.NewCoins(coin)); err != nil { + return err + } + + if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, transfertypes.ModuleName, userAccount, sdk.NewCoins(coin)); err != nil { + panic(fmt.Sprintf("unable to send coins from module to account despite previously minting coins to module account: %v", err)) + } + return nil + } + + escrowAddress := transfertypes.GetEscrowAddress(packet.SourcePort, packet.SourceChannel) + + if err := k.bankKeeper.SendCoins(ctx, escrowAddress, userAccount, sdk.NewCoins(coin)); err != nil { + return fmt.Errorf("failed to send coins from escrow account to user recoverable account: %w", err) + } + + // update the total escrow amount for the denom. + k.unescrowToken(ctx, coin) + + return nil +} + +// userRecoverableAccount finds an account on this chain that the original sender of the packet can recover funds from. +// If the destination receiver of the original packet is a valid bech32 address for this chain, we use that address. +// Otherwise, if the sender of the original packet is a valid bech32 address for another chain, we translate that address to this chain. +// Note that for the fallback, the coin type of the source chain sender account must be compatible with this chain. +func userRecoverableAccount(inFlightPacket *types.InFlightPacket) (sdk.AccAddress, error) { + var originalData transfertypes.FungibleTokenPacketData + err := transfertypes.ModuleCdc.UnmarshalJSON(inFlightPacket.PacketData, &originalData) + if err == nil { // if NO error + sender, err := sdk.AccAddressFromBech32(originalData.Receiver) + if err == nil { // if NO error + return sender, nil + } + } + + _, sender, fallbackErr := bech32.DecodeAndConvert(inFlightPacket.OriginalSenderAddress) + if fallbackErr == nil { // if NO error + return sender, nil + } + + return nil, fmt.Errorf("failed to decode bech32 addresses: %w", errors.Join(err, fallbackErr)) +} + +func (k *Keeper) WriteAcknowledgementForForwardedPacket(ctx sdk.Context, packet channeltypes.Packet, data transfertypes.FungibleTokenPacketData, inFlightPacket *types.InFlightPacket, ack channeltypes.Acknowledgement) error { + // Lookup module by channel capability + _, found := k.channelKeeper.GetChannel(ctx, inFlightPacket.RefundPortId, inFlightPacket.RefundChannelId) + if !found { + return errors.New("could not retrieve module from port-id") + } + + if ack.Success() { + return k.ics4Wrapper.WriteAcknowledgement(ctx, inFlightPacket.ChannelPacket(), ack) + } + + // For forwarded packets, the funds were moved into an escrow account if the denom originated on this chain. + // On an ack error or timeout on a forwarded packet, the funds in the escrow account + // should be moved to the other escrow account on the other side or burned. + + // If this packet is non-refundable due to some action that took place between the initial ibc transfer and the forward + // we write a successful ack containing details on what happened regardless of ack error or timeout + if inFlightPacket.Nonrefundable { + // We are not allowed to refund back to the source chain. + // attempt to move funds to user recoverable account on this chain. + if err := k.moveFundsToUserRecoverableAccount(ctx, packet, data, inFlightPacket); err != nil { + return err + } + + ackResult := fmt.Sprintf("packet forward failed after point of no return: %s", ack.GetError()) + newAck := channeltypes.NewResultAcknowledgement([]byte(ackResult)) + + return k.ics4Wrapper.WriteAcknowledgement(ctx, inFlightPacket.ChannelPacket(), newAck) + } + + fullDenomPath := data.Denom + var err error + + // Deconstruct the token denomination into the denomination trace info + // to determine if the sender is the source chain + if strings.HasPrefix(data.Denom, "ibc/") { + fullDenomPath, err = k.transferKeeper.DenomPathFromHash(ctx, data.Denom) + if err != nil { + return err + } + } + + amount, ok := sdkmath.NewIntFromString(data.Amount) + if !ok { + return fmt.Errorf("failed to parse amount from packet data for forward refund: %s", data.Amount) + } + + denom := transfertypes.ExtractDenomFromPath(fullDenomPath) + coin := sdk.NewCoin(denom.IBCDenom(), amount) + + escrowAddress := transfertypes.GetEscrowAddress(packet.SourcePort, packet.SourceChannel) + refundEscrowAddress := transfertypes.GetEscrowAddress(inFlightPacket.RefundPortId, inFlightPacket.RefundChannelId) + + newToken := sdk.NewCoins(coin) + + // Sender chain is source + if !denom.HasPrefix(packet.SourcePort, packet.SourceChannel) { + // funds were moved to escrow account for transfer, so they need to either: + // - move to the other escrow account, in the case of native denom + // - burn + if !denom.HasPrefix(inFlightPacket.RefundPortId, inFlightPacket.RefundChannelId) { + // transfer funds from escrow account for forwarded packet to escrow account going back for refund. + if err := k.bankKeeper.SendCoins(ctx, escrowAddress, refundEscrowAddress, newToken); err != nil { + return fmt.Errorf("failed to send coins from escrow account to refund escrow account: %w", err) + } + } else { + // Transfer the coins from the escrow account to the module account and burn them. + if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, escrowAddress, transfertypes.ModuleName, newToken); err != nil { + return fmt.Errorf("failed to send coins from escrow to module account for burn: %w", err) + } + + if err := k.bankKeeper.BurnCoins(ctx, transfertypes.ModuleName, newToken); err != nil { + // NOTE: should not happen as the module account was + // retrieved on the step above and it has enough balance + // to burn. + panic(fmt.Sprintf("cannot burn coins after a successful send from escrow account to module account: %v", err)) + } + + k.unescrowToken(ctx, coin) + } + } else { + // Funds in the escrow account were burned, + // so on a timeout or acknowledgement error we need to mint the funds back to the escrow account. + if err := k.bankKeeper.MintCoins(ctx, transfertypes.ModuleName, newToken); err != nil { + return fmt.Errorf("cannot mint coins to the %s module account: %v", transfertypes.ModuleName, err) + } + + if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, transfertypes.ModuleName, refundEscrowAddress, newToken); err != nil { + return fmt.Errorf("cannot send coins from the %s module to the escrow account %s: %v", transfertypes.ModuleName, refundEscrowAddress, err) + } + + currentTotalEscrow := k.transferKeeper.GetTotalEscrowForDenom(ctx, coin.GetDenom()) + newTotalEscrow := currentTotalEscrow.Add(coin) + k.transferKeeper.SetTotalEscrowForDenom(ctx, newTotalEscrow) + } + + return k.ics4Wrapper.WriteAcknowledgement(ctx, inFlightPacket.ChannelPacket(), ack) +} + +// unescrowToken will update the total escrow by deducting the unescrowed token +// from the current total escrow. +func (k *Keeper) unescrowToken(ctx sdk.Context, token sdk.Coin) { + currentTotalEscrow := k.transferKeeper.GetTotalEscrowForDenom(ctx, token.GetDenom()) + newTotalEscrow := currentTotalEscrow.Sub(token) + k.transferKeeper.SetTotalEscrowForDenom(ctx, newTotalEscrow) +} + +func (k *Keeper) ForwardTransferPacket(ctx sdk.Context, inFlightPacket *types.InFlightPacket, srcPacket channeltypes.Packet, srcPacketSender, receiver string, metadata *types.ForwardMetadata, token sdk.Coin, maxRetries uint8, timeoutDelta time.Duration, labels []metrics.Label, nonrefundable bool) error { + memo := "" + + // set memo for next transfer with next from this transfer. + if metadata.Next != nil { + memoBz, err := json.Marshal(metadata.Next) + if err != nil { + k.Logger(ctx).Error("packetForwardMiddleware error marshaling next as JSON", "error", err) + return errorsmod.Wrapf(sdkerrors.ErrJSONMarshal, err.Error()) + } + memo = string(memoBz) + } + + k.Logger(ctx).Debug("packetForwardMiddleware ForwardTransferPacket", + "port", metadata.Port, + "channel", metadata.Channel, + "sender", receiver, + "receiver", metadata.Receiver, + "amount", token.Amount.String(), + "denom", token.Denom, + ) + + msgTransfer := transfertypes.NewMsgTransfer(metadata.Port, metadata.Channel, token, receiver, metadata.Receiver, DefaultTransferPacketTimeoutHeight, uint64(ctx.BlockTime().UnixNano())+uint64(timeoutDelta.Nanoseconds()), memo) + // send tokens to destination + res, err := k.transferKeeper.Transfer(ctx, msgTransfer) + if err != nil { + k.Logger(ctx).Error("packetForwardMiddleware ForwardTransferPacket error", + "port", metadata.Port, + "channel", metadata.Channel, + "sender", receiver, + "receiver", metadata.Receiver, + "amount", token.Amount.String(), + "denom", token.Denom, + "error", err, + ) + return errorsmod.Wrapf(sdkerrors.ErrInsufficientFunds, err.Error()) + } + + // Store the following information in keeper: + // key - information about forwarded packet: src_channel (parsedReceiver.Channel), src_port (parsedReceiver.Port), sequence + // value - information about original packet for refunding if necessary: retries, srcPacketSender, srcPacket.DestinationChannel, srcPacket.DestinationPort + if inFlightPacket == nil { + inFlightPacket = &types.InFlightPacket{ + PacketData: srcPacket.Data, + OriginalSenderAddress: srcPacketSender, + RefundChannelId: srcPacket.DestinationChannel, + RefundPortId: srcPacket.DestinationPort, + RefundSequence: srcPacket.Sequence, + PacketSrcPortId: srcPacket.SourcePort, + PacketSrcChannelId: srcPacket.SourceChannel, + + PacketTimeoutTimestamp: srcPacket.TimeoutTimestamp, + PacketTimeoutHeight: srcPacket.TimeoutHeight.String(), + + RetriesRemaining: int32(maxRetries), + Timeout: uint64(timeoutDelta.Nanoseconds()), + Nonrefundable: nonrefundable, + } + } else { + inFlightPacket.RetriesRemaining-- + } + + key := types.RefundPacketKey(metadata.Channel, metadata.Port, res.Sequence) + store := k.storeService.OpenKVStore(ctx) + bz := k.cdc.MustMarshal(inFlightPacket) + if err := store.Set(key, bz); err != nil { + return err + } + + defer func() { + if token.Amount.IsInt64() { + telemetry.SetGaugeWithLabels([]string{"tx", "msg", "ibc", "transfer"}, float32(token.Amount.Int64()), []metrics.Label{telemetry.NewLabel(coremetrics.LabelDenom, token.Denom)}) + } + + telemetry.IncrCounterWithLabels([]string{"ibc", types.ModuleName, "send"}, 1, labels) + }() + return nil +} + +// TimeoutShouldRetry returns inFlightPacket and no error if retry should be attempted. Error is returned if IBC refund should occur. +func (k *Keeper) TimeoutShouldRetry(ctx sdk.Context, packet channeltypes.Packet) (*types.InFlightPacket, error) { + inFlightPacket, err := k.GetInflightPacket(ctx, packet) + if err != nil { + return nil, err + } + + // Not a forwarded packet. Ignore. + if inFlightPacket == nil { + return nil, nil + } + + if inFlightPacket.RetriesRemaining <= 0 { + key := types.RefundPacketKey(packet.SourceChannel, packet.SourcePort, packet.Sequence) + k.Logger(ctx).Error("packetForwardMiddleware reached max retries for packet", + "key", string(key), + "original-sender-address", inFlightPacket.OriginalSenderAddress, + "refund-channel-id", inFlightPacket.RefundChannelId, + "refund-port-id", inFlightPacket.RefundPortId, + ) + + return inFlightPacket, fmt.Errorf("giving up on packet on channel (%s) port (%s) after max retries", inFlightPacket.RefundChannelId, inFlightPacket.RefundPortId) + } + + return inFlightPacket, nil +} + +func (k *Keeper) RetryTimeout(ctx sdk.Context, channel, port string, data transfertypes.FungibleTokenPacketData, inFlightPacket *types.InFlightPacket) error { + // send transfer again + metadata := &types.ForwardMetadata{ + Receiver: data.Receiver, + Channel: channel, + Port: port, + } + + if data.Memo != "" { + metadata.Next = &types.JSONObject{} + if err := json.Unmarshal([]byte(data.Memo), metadata.Next); err != nil { + return fmt.Errorf("error unmarshaling memo json: %w", err) + } + } + + amount, ok := sdkmath.NewIntFromString(data.Amount) + if !ok { + k.Logger(ctx).Error("packetForwardMiddleware error parsing amount from string for packetforward retry on timeout", + "original-sender-address", inFlightPacket.OriginalSenderAddress, + "refund-channel-id", inFlightPacket.RefundChannelId, + "refund-port-id", inFlightPacket.RefundPortId, + "retries-remaining", inFlightPacket.RetriesRemaining, + "amount", data.Amount, + ) + return fmt.Errorf("error parsing amount from string for packetforward retry: %s", data.Amount) + } + + ibcDenom := transfertypes.ExtractDenomFromPath(data.Denom).IBCDenom() + + token := sdk.NewCoin(ibcDenom, amount) + + // srcPacket and srcPacketSender are empty because inFlightPacket is non-nil. + return k.ForwardTransferPacket(ctx, inFlightPacket, channeltypes.Packet{}, "", data.Sender, metadata, token, uint8(inFlightPacket.RetriesRemaining), time.Duration(inFlightPacket.Timeout)*time.Nanosecond, nil, inFlightPacket.Nonrefundable) +} + +func (k *Keeper) GetInflightPacket(ctx sdk.Context, packet channeltypes.Packet) (*types.InFlightPacket, error) { + store := k.storeService.OpenKVStore(ctx) + key := types.RefundPacketKey(packet.SourceChannel, packet.SourcePort, packet.Sequence) + bz, err := store.Get(key) + if err != nil { + return nil, err + } + if len(bz) == 0 { + return nil, nil + } + var inFlightPacket types.InFlightPacket + k.cdc.MustUnmarshal(bz, &inFlightPacket) + return &inFlightPacket, nil +} + +func (k *Keeper) RemoveInFlightPacket(ctx sdk.Context, packet channeltypes.Packet) { + store := k.storeService.OpenKVStore(ctx) + key := types.RefundPacketKey(packet.SourceChannel, packet.SourcePort, packet.Sequence) + hasKey, err := store.Has(key) + if err != nil { + panic(err) + } + if !hasKey { + // not a forwarded packet, ignore. + return + } + + // done with packet key now, delete. + if err := store.Delete(key); err != nil { + panic(err) + } +} + +// SendPacket wraps IBC ChannelKeeper's SendPacket function +func (k *Keeper) SendPacket(ctx sdk.Context, sourcePort, sourceChannel string, timeoutHeight clienttypes.Height, timeoutTimestamp uint64, data []byte) (sequence uint64, err error) { + return k.ics4Wrapper.SendPacket(ctx, sourcePort, sourceChannel, timeoutHeight, timeoutTimestamp, data) +} + +// WriteAcknowledgement wraps IBC ICS4Wrapper WriteAcknowledgement function. +// ICS29 WriteAcknowledgement is used for asynchronous acknowledgements. +func (k *Keeper) WriteAcknowledgement(ctx sdk.Context, packet ibcexported.PacketI, acknowledgement ibcexported.Acknowledgement) error { + return k.ics4Wrapper.WriteAcknowledgement(ctx, packet, acknowledgement) +} + +// WriteAcknowledgement wraps IBC ICS4Wrapper GetAppVersion function. +func (k *Keeper) GetAppVersion(ctx sdk.Context, portID, channelID string) (string, bool) { + return k.ics4Wrapper.GetAppVersion(ctx, portID, channelID) +} + +// LookupModuleByChannel wraps ChannelKeeper LookupModuleByChannel function. +func (k *Keeper) GetChannel(ctx sdk.Context, portID, channelID string) (channeltypes.Channel, bool) { + return k.channelKeeper.GetChannel(ctx, portID, channelID) +} diff --git a/modules/apps/packet-forward-middleware/keeper/migrator.go b/modules/apps/packet-forward-middleware/keeper/migrator.go new file mode 100644 index 00000000000..51aa202bb24 --- /dev/null +++ b/modules/apps/packet-forward-middleware/keeper/migrator.go @@ -0,0 +1,24 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + v3 "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/migrations/v3" +) + +// Migrator is a struct for handling in-place state migrations. +type Migrator struct { + keeper *Keeper +} + +func NewMigrator(k *Keeper) Migrator { + return Migrator{ + keeper: k, + } +} + +// Migrate2to3 migrates the module state from the consensus version 2 to +// version 3 +func (m Migrator) Migrate2to3(ctx sdk.Context) error { + return v3.Migrate(ctx, m.keeper.bankKeeper, m.keeper.channelKeeper, m.keeper.transferKeeper) +} diff --git a/modules/apps/packet-forward-middleware/migrations/v3/migrate.go b/modules/apps/packet-forward-middleware/migrations/v3/migrate.go new file mode 100644 index 00000000000..fd66d570475 --- /dev/null +++ b/modules/apps/packet-forward-middleware/migrations/v3/migrate.go @@ -0,0 +1,50 @@ +package v3 + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/types" + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" +) + +// Migrate migrates the x/packetforward module state from the consensus version +// 2 to version 3 +func Migrate(ctx sdk.Context, bankKeeper types.BankKeeper, channelKeeper types.ChannelKeeper, transferKeeper types.TransferKeeper) error { + logger := ctx.Logger() + + expectedTotalEscrowed := sdk.Coins{} + + // 1. Iterate over all IBC transfer channels + portID := transferKeeper.GetPort(ctx) + transferChannels := channelKeeper.GetAllChannelsWithPortPrefix(ctx, portID) + for _, channel := range transferChannels { + // 2. For each channel, get the escrow address and corresponding bank balance + escrowAddress := transfertypes.GetEscrowAddress(portID, channel.ChannelId) + bankBalances := bankKeeper.GetAllBalances(ctx, escrowAddress) + + // 3. Aggregate the bank balances to calculate the expected total escrowed + expectedTotalEscrowed = expectedTotalEscrowed.Add(bankBalances...) + } + + logger.Info( + "Calculated expected total escrowed from escrow account bank balances", + "num channels", len(transferChannels), + "bank total escrowed", expectedTotalEscrowed, + ) + + // 4. Set the total escrowed for each denom + for _, totalEscrowCoin := range expectedTotalEscrowed { + prevDenomEscrow := transferKeeper.GetTotalEscrowForDenom(ctx, totalEscrowCoin.Denom) + + transferKeeper.SetTotalEscrowForDenom(ctx, totalEscrowCoin) + + logger.Info( + "Corrected total escrow for denom to match escrow account bank balances", + "denom", totalEscrowCoin.Denom, + "previous escrow", prevDenomEscrow, + "new escrow", totalEscrowCoin, + ) + } + + return nil +} diff --git a/modules/apps/packet-forward-middleware/module.go b/modules/apps/packet-forward-middleware/module.go new file mode 100644 index 00000000000..a0f94e7b0e4 --- /dev/null +++ b/modules/apps/packet-forward-middleware/module.go @@ -0,0 +1,130 @@ +package packetforward + +import ( + "encoding/json" + "fmt" + + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + + abci "github.com/cometbft/cometbft/abci/types" + + "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/keeper" + "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/types" +) + +var ( + _ module.AppModule = AppModule{} + _ module.AppModuleBasic = AppModuleBasic{} +) + +// AppModuleBasic is the packetforward AppModuleBasic +type AppModuleBasic struct{} + +// Name implements AppModuleBasic interface +func (AppModuleBasic) Name() string { + return types.ModuleName +} + +// RegisterLegacyAminoCodec implements AppModuleBasic interface +func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { + types.RegisterLegacyAminoCodec(cdc) +} + +// RegisterInterfaces registers module concrete types into protobuf Any. +func (AppModuleBasic) RegisterInterfaces(r codectypes.InterfaceRegistry) { + types.RegisterInterfaces(r) +} + +// DefaultGenesis returns default genesis state as raw bytes for the ibc +// packetforward module. +func (AppModuleBasic) DefaultGenesis(cdc codec.JSONCodec) json.RawMessage { + return cdc.MustMarshalJSON(types.DefaultGenesisState()) +} + +// ValidateGenesis performs genesis state validation for the packetforward module. +func (AppModuleBasic) ValidateGenesis(cdc codec.JSONCodec, _ client.TxEncodingConfig, bz json.RawMessage) error { + var gs types.GenesisState + if err := cdc.UnmarshalJSON(bz, &gs); err != nil { + return fmt.Errorf("failed to unmarshal %s genesis state: %w", types.ModuleName, err) + } + + return gs.Validate() +} + +// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the packetforward module. +func (AppModuleBasic) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *runtime.ServeMux) { +} + +// GetTxCmd implements AppModuleBasic interface +func (AppModuleBasic) GetTxCmd() *cobra.Command { + return nil +} + +// GetQueryCmd implements AppModuleBasic interface +func (AppModuleBasic) GetQueryCmd() *cobra.Command { + return nil +} + +// AppModule represents the AppModule for this module +type AppModule struct { + AppModuleBasic + keeper *keeper.Keeper +} + +// NewAppModule creates a new packetforward module +func NewAppModule(k *keeper.Keeper) AppModule { + return AppModule{ + keeper: k, + } +} + +// IsOnePerModuleType implements the depinject.OnePerModuleType interface. +func (AppModule) IsOnePerModuleType() {} + +// IsAppModule implements the appmodule.AppModule interface. +func (AppModule) IsAppModule() {} + +// QuerierRoute implements the AppModule interface +func (AppModule) QuerierRoute() string { + return types.QuerierRoute +} + +// RegisterServices registers module services. +func (am AppModule) RegisterServices(cfg module.Configurator) { + m := keeper.NewMigrator(am.keeper) + if err := cfg.RegisterMigration(types.ModuleName, 2, m.Migrate2to3); err != nil { + panic(fmt.Sprintf("failed to migrate x/%s from version 2 to 3: %v", types.ModuleName, err)) + } +} + +// InitGenesis performs genesis initialization for the packetforward module. It returns +// no validator updates. +func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) []abci.ValidatorUpdate { + err := am.ValidateGenesis(cdc, nil, data) + if err != nil { + panic(err) + } + + var genesisState types.GenesisState + cdc.MustUnmarshalJSON(data, &genesisState) + am.keeper.InitGenesis(ctx, genesisState) + + return []abci.ValidatorUpdate{} +} + +// ExportGenesis returns the exported genesis state as raw bytes for the packetforward +// module. +func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage { + gs := am.keeper.ExportGenesis(ctx) + return cdc.MustMarshalJSON(gs) +} + +// ConsensusVersion implements AppModule/ConsensusVersion. +func (AppModule) ConsensusVersion() uint64 { return 3 } diff --git a/modules/apps/packet-forward-middleware/types/codec.go b/modules/apps/packet-forward-middleware/types/codec.go new file mode 100644 index 00000000000..3a4f6648512 --- /dev/null +++ b/modules/apps/packet-forward-middleware/types/codec.go @@ -0,0 +1,28 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var amino = codec.NewLegacyAmino() + +func init() { + RegisterLegacyAminoCodec(amino) + cryptocodec.RegisterCrypto(amino) + sdk.RegisterLegacyAminoCodec(amino) + + // Register all Amino interfaces and concrete types on the authz Amino codec + // so that this can later be used to properly serialize MsgGrant and MsgExec + // instances. + // RegisterLegacyAminoCodec(authzcodec.Amino) // TODO(bez): Investigate this. +} + +// RegisterLegacyAminoCodec registers concrete types on the LegacyAmino codec +func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { +} + +func RegisterInterfaces(registry types.InterfaceRegistry) { +} diff --git a/modules/apps/packet-forward-middleware/types/expected_keepers.go b/modules/apps/packet-forward-middleware/types/expected_keepers.go new file mode 100644 index 00000000000..0b48938127d --- /dev/null +++ b/modules/apps/packet-forward-middleware/types/expected_keepers.go @@ -0,0 +1,44 @@ +package types + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + + cmtbytes "github.com/cometbft/cometbft/libs/bytes" + + transfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" + channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" +) + +// TransferKeeper defines the expected transfer keeper +type TransferKeeper interface { + Transfer(ctx context.Context, msg *transfertypes.MsgTransfer) (*transfertypes.MsgTransferResponse, error) + GetDenom(ctx sdk.Context, denomHash cmtbytes.HexBytes) (transfertypes.Denom, bool) + GetTotalEscrowForDenom(ctx sdk.Context, denom string) sdk.Coin + SetTotalEscrowForDenom(ctx sdk.Context, coin sdk.Coin) + DenomPathFromHash(ctx sdk.Context, ibcDenom string) (string, error) + + // Only used for v3 migration + GetPort(ctx sdk.Context) string +} + +// ChannelKeeper defines the expected IBC channel keeper +type ChannelKeeper interface { + GetChannel(ctx sdk.Context, srcPort, srcChan string) (channel channeltypes.Channel, found bool) + + // Only used for v3 migration + GetAllChannelsWithPortPrefix(ctx sdk.Context, portPrefix string) []channeltypes.IdentifiedChannel +} + +// BankKeeper defines the expected bank keeper +type BankKeeper interface { + SendCoins(ctx context.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error + SendCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error + SendCoinsFromAccountToModule(ctx context.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error + MintCoins(ctx context.Context, moduleName string, amt sdk.Coins) error + BurnCoins(ctx context.Context, moduleName string, amt sdk.Coins) error + + // Only used for v3 migration + GetAllBalances(ctx context.Context, addr sdk.AccAddress) sdk.Coins +} diff --git a/modules/apps/packet-forward-middleware/types/forward.go b/modules/apps/packet-forward-middleware/types/forward.go new file mode 100644 index 00000000000..7f3a12a992e --- /dev/null +++ b/modules/apps/packet-forward-middleware/types/forward.go @@ -0,0 +1,117 @@ +package types + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/iancoleman/orderedmap" + + host "github.com/cosmos/ibc-go/v10/modules/core/24-host" +) + +type PacketMetadata struct { + Forward *ForwardMetadata `json:"forward"` +} + +type ForwardMetadata struct { + Receiver string `json:"receiver,omitempty"` + Port string `json:"port,omitempty"` + Channel string `json:"channel,omitempty"` + Timeout Duration `json:"timeout,omitempty"` + Retries *uint8 `json:"retries,omitempty"` + + // Using JSONObject so that objects for next property will not be mutated by golang's lexicographic key sort on map keys during Marshal. + // Supports primitives for Unmarshal/Marshal so that an escaped JSON-marshaled string is also valid. + Next *JSONObject `json:"next,omitempty"` +} + +type Duration time.Duration + +func (m *ForwardMetadata) Validate() error { + if m.Receiver == "" { + return errors.New("failed to validate metadata. receiver cannot be empty") + } + if err := host.PortIdentifierValidator(m.Port); err != nil { + return fmt.Errorf("failed to validate metadata: %w", err) + } + if err := host.ChannelIdentifierValidator(m.Channel); err != nil { + return fmt.Errorf("failed to validate metadata: %w", err) + } + + return nil +} + +// JSONObject is a wrapper type to allow either a primitive type or a JSON object. +// In the case the value is a JSON object, OrderedMap type is used so that key order +// is retained across Unmarshal/Marshal. +type JSONObject struct { + obj bool + primitive []byte + orderedMap orderedmap.OrderedMap +} + +// NewJSONObject is a constructor used for tests. +// The usage of JSONObject in the middleware is only json Marshal/Unmarshal +func NewJSONObject(object bool, primitive []byte, orderedMap orderedmap.OrderedMap) *JSONObject { + return &JSONObject{ + obj: object, + primitive: primitive, + orderedMap: orderedMap, + } +} + +// UnmarshalJSON overrides the default json.Unmarshal behavior +func (o *JSONObject) UnmarshalJSON(b []byte) error { + if err := o.orderedMap.UnmarshalJSON(b); err != nil { + // If ordered map unmarshal fails, this is a primitive value + o.obj = false + // Attempt to unmarshal as string, this removes extra JSON escaping + var primitiveStr string + if err := json.Unmarshal(b, &primitiveStr); err != nil { + o.primitive = b + return nil + } + o.primitive = []byte(primitiveStr) + return nil + } + // This is a JSON object, now stored as an ordered map to retain key order. + o.obj = true + return nil +} + +// MarshalJSON overrides the default json.Marshal behavior +func (o JSONObject) MarshalJSON() ([]byte, error) { + if o.obj { + // non-primitive, return marshaled ordered map. + return o.orderedMap.MarshalJSON() + } + // primitive, return raw bytes. + return o.primitive, nil +} + +func (d *Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(time.Duration(*d).Nanoseconds()) +} + +func (d *Duration) UnmarshalJSON(b []byte) error { + var v any + if err := json.Unmarshal(b, &v); err != nil { + return err + } + switch value := v.(type) { + case float64: + *d = Duration(time.Duration(value)) + return nil + case string: + tmp, err := time.ParseDuration(value) + if err != nil { + return err + } + *d = Duration(tmp) + return nil + default: + return errors.New("invalid duration") + } +} diff --git a/modules/apps/packet-forward-middleware/types/forward_test.go b/modules/apps/packet-forward-middleware/types/forward_test.go new file mode 100644 index 00000000000..479dc9b6835 --- /dev/null +++ b/modules/apps/packet-forward-middleware/types/forward_test.go @@ -0,0 +1,60 @@ +package types_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/types" +) + +func TestForwardMetadataUnmarshalStringNext(t *testing.T) { + const memo = "{\"forward\":{\"receiver\":\"noble1f4cur2krsua2th9kkp7n0zje4stea4p9tu70u8\",\"port\":\"transfer\",\"channel\":\"channel-0\",\"timeout\":0,\"next\":\"{\\\"forward\\\":{\\\"receiver\\\":\\\"noble1l505zhahp24v5jsmps9vs5asah759fdce06sfp\\\",\\\"port\\\":\\\"transfer\\\",\\\"channel\\\":\\\"channel-0\\\",\\\"timeout\\\":0}}\"}}" + var packetMetadata types.PacketMetadata + + err := json.Unmarshal([]byte(memo), &packetMetadata) + require.NoError(t, err) + + nextBz, err := json.Marshal(packetMetadata.Forward.Next) + require.NoError(t, err) + require.Equal(t, `{"forward":{"receiver":"noble1l505zhahp24v5jsmps9vs5asah759fdce06sfp","port":"transfer","channel":"channel-0","timeout":0}}`, string(nextBz)) +} + +func TestForwardMetadataUnmarshalJSONNext(t *testing.T) { + const memo = "{\"forward\":{\"receiver\":\"noble1f4cur2krsua2th9kkp7n0zje4stea4p9tu70u8\",\"port\":\"transfer\",\"channel\":\"channel-0\",\"timeout\":0,\"next\":{\"forward\":{\"receiver\":\"noble1l505zhahp24v5jsmps9vs5asah759fdce06sfp\",\"port\":\"transfer\",\"channel\":\"channel-0\",\"timeout\":0}}}}" + var packetMetadata types.PacketMetadata + + err := json.Unmarshal([]byte(memo), &packetMetadata) + require.NoError(t, err) + + nextBz, err := json.Marshal(packetMetadata.Forward.Next) + require.NoError(t, err) + require.Equal(t, `{"forward":{"receiver":"noble1l505zhahp24v5jsmps9vs5asah759fdce06sfp","port":"transfer","channel":"channel-0","timeout":0}}`, string(nextBz)) +} + +func TestTimeoutUnmarshalString(t *testing.T) { + const memo = "{\"forward\":{\"receiver\":\"noble1f4cur2krsua2th9kkp7n0zje4stea4p9tu70u8\",\"port\":\"transfer\",\"channel\":\"channel-0\",\"timeout\":\"60s\"}}" + var packetMetadata types.PacketMetadata + + err := json.Unmarshal([]byte(memo), &packetMetadata) + require.NoError(t, err) + + timeoutBz, err := json.Marshal(packetMetadata.Forward.Timeout) + require.NoError(t, err) + + require.Equal(t, "60000000000", string(timeoutBz)) +} + +func TestTimeoutUnmarshalJSON(t *testing.T) { + const memo = "{\"forward\":{\"receiver\":\"noble1f4cur2krsua2th9kkp7n0zje4stea4p9tu70u8\",\"port\":\"transfer\",\"channel\":\"channel-0\",\"timeout\": 60000000000}}" + var packetMetadata types.PacketMetadata + + err := json.Unmarshal([]byte(memo), &packetMetadata) + require.NoError(t, err) + + timeoutBz, err := json.Marshal(packetMetadata.Forward.Timeout) + require.NoError(t, err) + + require.Equal(t, "60000000000", string(timeoutBz)) +} diff --git a/modules/apps/packet-forward-middleware/types/genesis.go b/modules/apps/packet-forward-middleware/types/genesis.go new file mode 100644 index 00000000000..41d18f3e1ee --- /dev/null +++ b/modules/apps/packet-forward-middleware/types/genesis.go @@ -0,0 +1,19 @@ +package types + +import "errors" + +// DefaultGenesisState returns a GenesisState with an empty map of in-flight packets. +func DefaultGenesisState() *GenesisState { + return &GenesisState{ + InFlightPackets: make(map[string]InFlightPacket), + } +} + +// Validate performs basic genesis state validation returning an error upon any failure. +func (gs GenesisState) Validate() error { + if gs.InFlightPackets == nil { + return errors.New("in-flight packets cannot be nil") + } + + return nil +} diff --git a/modules/apps/packet-forward-middleware/types/genesis.pb.go b/modules/apps/packet-forward-middleware/types/genesis.pb.go new file mode 100644 index 00000000000..42e605ca3ff --- /dev/null +++ b/modules/apps/packet-forward-middleware/types/genesis.pb.go @@ -0,0 +1,1131 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: ibc/applications/packet_forward_middleware/v1/genesis.proto + +package types + +import ( + fmt "fmt" + _ "github.com/cosmos/gogoproto/gogoproto" + proto "github.com/cosmos/gogoproto/proto" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// GenesisState defines the packetforward genesis state +type GenesisState struct { + // key - information about forwarded packet: src_channel + // (parsedReceiver.Channel), src_port (parsedReceiver.Port), sequence value - + // information about original packet for refunding if necessary: retries, + // srcPacketSender, srcPacket.DestinationChannel, srcPacket.DestinationPort + InFlightPackets map[string]InFlightPacket `protobuf:"bytes,2,rep,name=in_flight_packets,json=inFlightPackets,proto3" json:"in_flight_packets" yaml:"in_flight_packets" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (m *GenesisState) Reset() { *m = GenesisState{} } +func (m *GenesisState) String() string { return proto.CompactTextString(m) } +func (*GenesisState) ProtoMessage() {} +func (*GenesisState) Descriptor() ([]byte, []int) { + return fileDescriptor_421a822166afb238, []int{0} +} +func (m *GenesisState) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *GenesisState) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_GenesisState.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *GenesisState) XXX_Merge(src proto.Message) { + xxx_messageInfo_GenesisState.Merge(m, src) +} +func (m *GenesisState) XXX_Size() int { + return m.Size() +} +func (m *GenesisState) XXX_DiscardUnknown() { + xxx_messageInfo_GenesisState.DiscardUnknown(m) +} + +var xxx_messageInfo_GenesisState proto.InternalMessageInfo + +func (m *GenesisState) GetInFlightPackets() map[string]InFlightPacket { + if m != nil { + return m.InFlightPackets + } + return nil +} + +// InFlightPacket contains information about original packet for +// writing the acknowledgement and refunding if necessary. +type InFlightPacket struct { + OriginalSenderAddress string `protobuf:"bytes,1,opt,name=original_sender_address,json=originalSenderAddress,proto3" json:"original_sender_address,omitempty"` + RefundChannelId string `protobuf:"bytes,2,opt,name=refund_channel_id,json=refundChannelId,proto3" json:"refund_channel_id,omitempty"` + RefundPortId string `protobuf:"bytes,3,opt,name=refund_port_id,json=refundPortId,proto3" json:"refund_port_id,omitempty"` + PacketSrcChannelId string `protobuf:"bytes,4,opt,name=packet_src_channel_id,json=packetSrcChannelId,proto3" json:"packet_src_channel_id,omitempty"` + PacketSrcPortId string `protobuf:"bytes,5,opt,name=packet_src_port_id,json=packetSrcPortId,proto3" json:"packet_src_port_id,omitempty"` + PacketTimeoutTimestamp uint64 `protobuf:"varint,6,opt,name=packet_timeout_timestamp,json=packetTimeoutTimestamp,proto3" json:"packet_timeout_timestamp,omitempty"` + PacketTimeoutHeight string `protobuf:"bytes,7,opt,name=packet_timeout_height,json=packetTimeoutHeight,proto3" json:"packet_timeout_height,omitempty"` + PacketData []byte `protobuf:"bytes,8,opt,name=packet_data,json=packetData,proto3" json:"packet_data,omitempty"` + RefundSequence uint64 `protobuf:"varint,9,opt,name=refund_sequence,json=refundSequence,proto3" json:"refund_sequence,omitempty"` + RetriesRemaining int32 `protobuf:"varint,10,opt,name=retries_remaining,json=retriesRemaining,proto3" json:"retries_remaining,omitempty"` + Timeout uint64 `protobuf:"varint,11,opt,name=timeout,proto3" json:"timeout,omitempty"` + Nonrefundable bool `protobuf:"varint,12,opt,name=nonrefundable,proto3" json:"nonrefundable,omitempty"` +} + +func (m *InFlightPacket) Reset() { *m = InFlightPacket{} } +func (m *InFlightPacket) String() string { return proto.CompactTextString(m) } +func (*InFlightPacket) ProtoMessage() {} +func (*InFlightPacket) Descriptor() ([]byte, []int) { + return fileDescriptor_421a822166afb238, []int{1} +} +func (m *InFlightPacket) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *InFlightPacket) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_InFlightPacket.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *InFlightPacket) XXX_Merge(src proto.Message) { + xxx_messageInfo_InFlightPacket.Merge(m, src) +} +func (m *InFlightPacket) XXX_Size() int { + return m.Size() +} +func (m *InFlightPacket) XXX_DiscardUnknown() { + xxx_messageInfo_InFlightPacket.DiscardUnknown(m) +} + +var xxx_messageInfo_InFlightPacket proto.InternalMessageInfo + +func (m *InFlightPacket) GetOriginalSenderAddress() string { + if m != nil { + return m.OriginalSenderAddress + } + return "" +} + +func (m *InFlightPacket) GetRefundChannelId() string { + if m != nil { + return m.RefundChannelId + } + return "" +} + +func (m *InFlightPacket) GetRefundPortId() string { + if m != nil { + return m.RefundPortId + } + return "" +} + +func (m *InFlightPacket) GetPacketSrcChannelId() string { + if m != nil { + return m.PacketSrcChannelId + } + return "" +} + +func (m *InFlightPacket) GetPacketSrcPortId() string { + if m != nil { + return m.PacketSrcPortId + } + return "" +} + +func (m *InFlightPacket) GetPacketTimeoutTimestamp() uint64 { + if m != nil { + return m.PacketTimeoutTimestamp + } + return 0 +} + +func (m *InFlightPacket) GetPacketTimeoutHeight() string { + if m != nil { + return m.PacketTimeoutHeight + } + return "" +} + +func (m *InFlightPacket) GetPacketData() []byte { + if m != nil { + return m.PacketData + } + return nil +} + +func (m *InFlightPacket) GetRefundSequence() uint64 { + if m != nil { + return m.RefundSequence + } + return 0 +} + +func (m *InFlightPacket) GetRetriesRemaining() int32 { + if m != nil { + return m.RetriesRemaining + } + return 0 +} + +func (m *InFlightPacket) GetTimeout() uint64 { + if m != nil { + return m.Timeout + } + return 0 +} + +func (m *InFlightPacket) GetNonrefundable() bool { + if m != nil { + return m.Nonrefundable + } + return false +} + +func init() { + proto.RegisterType((*GenesisState)(nil), "ibc.applications.packet_forward_middleware.v1.GenesisState") + proto.RegisterMapType((map[string]InFlightPacket)(nil), "ibc.applications.packet_forward_middleware.v1.GenesisState.InFlightPacketsEntry") + proto.RegisterType((*InFlightPacket)(nil), "ibc.applications.packet_forward_middleware.v1.InFlightPacket") +} + +func init() { + proto.RegisterFile("ibc/applications/packet_forward_middleware/v1/genesis.proto", fileDescriptor_421a822166afb238) +} + +var fileDescriptor_421a822166afb238 = []byte{ + // 597 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x94, 0x4b, 0x6e, 0xdb, 0x3c, + 0x14, 0x85, 0x4d, 0xe7, 0x4d, 0xfb, 0xcf, 0x83, 0x7f, 0xd2, 0x12, 0x19, 0x38, 0x82, 0x11, 0xa0, + 0x42, 0x03, 0x4b, 0x75, 0x0a, 0x14, 0x41, 0x8a, 0x0e, 0x9a, 0x3e, 0x3d, 0x0b, 0xe4, 0x8c, 0x3a, + 0x11, 0x68, 0x89, 0x91, 0x89, 0x48, 0xa4, 0x4a, 0xd2, 0x0e, 0x3c, 0xec, 0x0e, 0xba, 0x82, 0x76, + 0x11, 0xdd, 0x44, 0x86, 0x19, 0x76, 0x14, 0x14, 0xf1, 0x0e, 0xba, 0x82, 0x42, 0x22, 0x95, 0xda, + 0x68, 0x3b, 0xc8, 0xc8, 0xf4, 0x3d, 0xf7, 0x7c, 0xf7, 0xe8, 0x42, 0x22, 0x7c, 0xce, 0x06, 0x91, + 0x4f, 0xf2, 0x3c, 0x65, 0x11, 0xd1, 0x4c, 0x70, 0xe5, 0xe7, 0x24, 0xba, 0xa0, 0x3a, 0x3c, 0x17, + 0xf2, 0x92, 0xc8, 0x38, 0xcc, 0x58, 0x1c, 0xa7, 0xf4, 0x92, 0x48, 0xea, 0x8f, 0xbb, 0x7e, 0x42, + 0x39, 0x55, 0x4c, 0x79, 0xb9, 0x14, 0x5a, 0xa0, 0x0e, 0x1b, 0x44, 0xde, 0xac, 0xd9, 0xfb, 0xa7, + 0xd9, 0x1b, 0x77, 0x77, 0xb7, 0x13, 0x91, 0x88, 0xd2, 0xe9, 0x17, 0x27, 0x03, 0x69, 0x7f, 0xab, + 0xc3, 0xe6, 0x3b, 0x83, 0xed, 0x6b, 0xa2, 0x29, 0xfa, 0x02, 0xe0, 0x16, 0xe3, 0xe1, 0x79, 0xca, + 0x92, 0xa1, 0x0e, 0x0d, 0x51, 0xe1, 0xba, 0xb3, 0xe0, 0x36, 0x0e, 0x4f, 0xbd, 0x7b, 0x8d, 0xf4, + 0x66, 0xc1, 0x5e, 0x8f, 0xbf, 0x2d, 0x99, 0xa7, 0x06, 0xf9, 0x86, 0x6b, 0x39, 0x39, 0x71, 0xae, + 0x6e, 0xf6, 0x6a, 0x3f, 0x6f, 0xf6, 0xf0, 0x84, 0x64, 0xe9, 0x71, 0xfb, 0x8f, 0xc1, 0xed, 0x60, + 0x83, 0xcd, 0xfb, 0x76, 0x3f, 0x01, 0xb8, 0xfd, 0x37, 0x16, 0xda, 0x84, 0x0b, 0x17, 0x74, 0x82, + 0x81, 0x03, 0xdc, 0xb5, 0xa0, 0x38, 0xa2, 0x3e, 0x5c, 0x1a, 0x93, 0x74, 0x44, 0x71, 0xdd, 0x01, + 0x6e, 0xe3, 0xf0, 0xc5, 0x3d, 0xe3, 0xcf, 0x4f, 0x09, 0x0c, 0xeb, 0xb8, 0x7e, 0x04, 0xda, 0x5f, + 0x17, 0xe1, 0xfa, 0xbc, 0x8a, 0x9e, 0xc1, 0x87, 0x42, 0xb2, 0x84, 0x71, 0x92, 0x86, 0x8a, 0xf2, + 0x98, 0xca, 0x90, 0xc4, 0xb1, 0xa4, 0x4a, 0xd9, 0x44, 0x3b, 0x95, 0xdc, 0x2f, 0xd5, 0x97, 0x46, + 0x44, 0x8f, 0xe1, 0x96, 0xa4, 0xe7, 0x23, 0x1e, 0x87, 0xd1, 0x90, 0x70, 0x4e, 0xd3, 0x90, 0xc5, + 0x65, 0xde, 0xb5, 0x60, 0xc3, 0x08, 0xaf, 0x4c, 0xbd, 0x17, 0xa3, 0x7d, 0xb8, 0x6e, 0x7b, 0x73, + 0x21, 0x75, 0xd1, 0xb8, 0x50, 0x36, 0x36, 0x4d, 0xf5, 0x54, 0x48, 0xdd, 0x8b, 0x51, 0x17, 0xee, + 0xd8, 0xc7, 0x52, 0x32, 0x9a, 0xa5, 0x2e, 0x96, 0xcd, 0xc8, 0x88, 0x7d, 0x19, 0xfd, 0x06, 0x1f, + 0x40, 0x34, 0x63, 0xa9, 0xe0, 0x4b, 0x26, 0xc5, 0x5d, 0xbf, 0xe5, 0x1f, 0x41, 0x6c, 0x9b, 0x35, + 0xcb, 0xa8, 0x18, 0x99, 0x5f, 0xa5, 0x49, 0x96, 0xe3, 0x65, 0x07, 0xb8, 0x8b, 0xc1, 0x03, 0xa3, + 0x9f, 0x19, 0xf9, 0xac, 0x52, 0xd1, 0xe1, 0x5d, 0xb2, 0xca, 0x39, 0xa4, 0xc5, 0x0a, 0xf1, 0x4a, + 0x39, 0xe9, 0xff, 0x39, 0xdb, 0xfb, 0x52, 0x42, 0x7b, 0xb0, 0x61, 0x3d, 0x31, 0xd1, 0x04, 0xaf, + 0x3a, 0xc0, 0x6d, 0x06, 0xd0, 0x94, 0x5e, 0x13, 0x4d, 0xd0, 0x23, 0x68, 0xf7, 0x14, 0x2a, 0xfa, + 0x71, 0x44, 0x79, 0x44, 0xf1, 0x5a, 0x99, 0xc2, 0xee, 0xaa, 0x6f, 0xab, 0xe8, 0xa0, 0xd8, 0xb4, + 0x96, 0x8c, 0xaa, 0x50, 0xd2, 0x8c, 0x30, 0xce, 0x78, 0x82, 0xa1, 0x03, 0xdc, 0xa5, 0x60, 0xd3, + 0x0a, 0x41, 0x55, 0x47, 0x18, 0xae, 0xd8, 0x8c, 0xb8, 0x51, 0xd2, 0xaa, 0xbf, 0x68, 0x1f, 0xfe, + 0xc7, 0x05, 0x37, 0x6c, 0x32, 0x48, 0x29, 0x6e, 0x3a, 0xc0, 0x5d, 0x0d, 0xe6, 0x8b, 0x27, 0xd1, + 0xd5, 0x6d, 0x0b, 0x5c, 0xdf, 0xb6, 0xc0, 0x8f, 0xdb, 0x16, 0xf8, 0x3c, 0x6d, 0xd5, 0xae, 0xa7, + 0xad, 0xda, 0xf7, 0x69, 0xab, 0xf6, 0xa1, 0x97, 0x30, 0x3d, 0x1c, 0x0d, 0xbc, 0x48, 0x64, 0x7e, + 0x24, 0x54, 0x26, 0x94, 0xcf, 0x06, 0x51, 0x27, 0x11, 0xfe, 0xb8, 0xfb, 0xc4, 0xcf, 0x44, 0x3c, + 0x4a, 0xa9, 0x2a, 0x2e, 0x85, 0xea, 0x32, 0xe8, 0xd8, 0xb7, 0xb3, 0x33, 0x73, 0x19, 0xe8, 0x49, + 0x4e, 0xd5, 0x60, 0xb9, 0xfc, 0x86, 0x9f, 0xfe, 0x0a, 0x00, 0x00, 0xff, 0xff, 0x12, 0x34, 0xfe, + 0x19, 0x47, 0x04, 0x00, 0x00, +} + +func (m *GenesisState) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GenesisState) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *GenesisState) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.InFlightPackets) > 0 { + for k := range m.InFlightPackets { + v := m.InFlightPackets[k] + baseI := i + { + size, err := (&v).MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenesis(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x12 + i -= len(k) + copy(dAtA[i:], k) + i = encodeVarintGenesis(dAtA, i, uint64(len(k))) + i-- + dAtA[i] = 0xa + i = encodeVarintGenesis(dAtA, i, uint64(baseI-i)) + i-- + dAtA[i] = 0x12 + } + } + return len(dAtA) - i, nil +} + +func (m *InFlightPacket) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *InFlightPacket) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *InFlightPacket) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Nonrefundable { + i-- + if m.Nonrefundable { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x60 + } + if m.Timeout != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.Timeout)) + i-- + dAtA[i] = 0x58 + } + if m.RetriesRemaining != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.RetriesRemaining)) + i-- + dAtA[i] = 0x50 + } + if m.RefundSequence != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.RefundSequence)) + i-- + dAtA[i] = 0x48 + } + if len(m.PacketData) > 0 { + i -= len(m.PacketData) + copy(dAtA[i:], m.PacketData) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.PacketData))) + i-- + dAtA[i] = 0x42 + } + if len(m.PacketTimeoutHeight) > 0 { + i -= len(m.PacketTimeoutHeight) + copy(dAtA[i:], m.PacketTimeoutHeight) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.PacketTimeoutHeight))) + i-- + dAtA[i] = 0x3a + } + if m.PacketTimeoutTimestamp != 0 { + i = encodeVarintGenesis(dAtA, i, uint64(m.PacketTimeoutTimestamp)) + i-- + dAtA[i] = 0x30 + } + if len(m.PacketSrcPortId) > 0 { + i -= len(m.PacketSrcPortId) + copy(dAtA[i:], m.PacketSrcPortId) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.PacketSrcPortId))) + i-- + dAtA[i] = 0x2a + } + if len(m.PacketSrcChannelId) > 0 { + i -= len(m.PacketSrcChannelId) + copy(dAtA[i:], m.PacketSrcChannelId) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.PacketSrcChannelId))) + i-- + dAtA[i] = 0x22 + } + if len(m.RefundPortId) > 0 { + i -= len(m.RefundPortId) + copy(dAtA[i:], m.RefundPortId) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.RefundPortId))) + i-- + dAtA[i] = 0x1a + } + if len(m.RefundChannelId) > 0 { + i -= len(m.RefundChannelId) + copy(dAtA[i:], m.RefundChannelId) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.RefundChannelId))) + i-- + dAtA[i] = 0x12 + } + if len(m.OriginalSenderAddress) > 0 { + i -= len(m.OriginalSenderAddress) + copy(dAtA[i:], m.OriginalSenderAddress) + i = encodeVarintGenesis(dAtA, i, uint64(len(m.OriginalSenderAddress))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func encodeVarintGenesis(dAtA []byte, offset int, v uint64) int { + offset -= sovGenesis(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *GenesisState) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.InFlightPackets) > 0 { + for k, v := range m.InFlightPackets { + _ = k + _ = v + l = v.Size() + mapEntrySize := 1 + len(k) + sovGenesis(uint64(len(k))) + 1 + l + sovGenesis(uint64(l)) + n += mapEntrySize + 1 + sovGenesis(uint64(mapEntrySize)) + } + } + return n +} + +func (m *InFlightPacket) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.OriginalSenderAddress) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + l = len(m.RefundChannelId) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + l = len(m.RefundPortId) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + l = len(m.PacketSrcChannelId) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + l = len(m.PacketSrcPortId) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + if m.PacketTimeoutTimestamp != 0 { + n += 1 + sovGenesis(uint64(m.PacketTimeoutTimestamp)) + } + l = len(m.PacketTimeoutHeight) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + l = len(m.PacketData) + if l > 0 { + n += 1 + l + sovGenesis(uint64(l)) + } + if m.RefundSequence != 0 { + n += 1 + sovGenesis(uint64(m.RefundSequence)) + } + if m.RetriesRemaining != 0 { + n += 1 + sovGenesis(uint64(m.RetriesRemaining)) + } + if m.Timeout != 0 { + n += 1 + sovGenesis(uint64(m.Timeout)) + } + if m.Nonrefundable { + n += 2 + } + return n +} + +func sovGenesis(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozGenesis(x uint64) (n int) { + return sovGenesis(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *GenesisState) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GenesisState: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GenesisState: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field InFlightPackets", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.InFlightPackets == nil { + m.InFlightPackets = make(map[string]InFlightPacket) + } + var mapkey string + mapvalue := &InFlightPacket{} + for iNdEx < postIndex { + entryPreIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + if fieldNum == 1 { + var stringLenmapkey uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLenmapkey |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLenmapkey := int(stringLenmapkey) + if intStringLenmapkey < 0 { + return ErrInvalidLengthGenesis + } + postStringIndexmapkey := iNdEx + intStringLenmapkey + if postStringIndexmapkey < 0 { + return ErrInvalidLengthGenesis + } + if postStringIndexmapkey > l { + return io.ErrUnexpectedEOF + } + mapkey = string(dAtA[iNdEx:postStringIndexmapkey]) + iNdEx = postStringIndexmapkey + } else if fieldNum == 2 { + var mapmsglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + mapmsglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if mapmsglen < 0 { + return ErrInvalidLengthGenesis + } + postmsgIndex := iNdEx + mapmsglen + if postmsgIndex < 0 { + return ErrInvalidLengthGenesis + } + if postmsgIndex > l { + return io.ErrUnexpectedEOF + } + mapvalue = &InFlightPacket{} + if err := mapvalue.Unmarshal(dAtA[iNdEx:postmsgIndex]); err != nil { + return err + } + iNdEx = postmsgIndex + } else { + iNdEx = entryPreIndex + skippy, err := skipGenesis(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenesis + } + if (iNdEx + skippy) > postIndex { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + m.InFlightPackets[mapkey] = *mapvalue + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipGenesis(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenesis + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *InFlightPacket) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: InFlightPacket: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: InFlightPacket: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field OriginalSenderAddress", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.OriginalSenderAddress = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field RefundChannelId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.RefundChannelId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field RefundPortId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.RefundPortId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field PacketSrcChannelId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.PacketSrcChannelId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field PacketSrcPortId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.PacketSrcPortId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 6: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field PacketTimeoutTimestamp", wireType) + } + m.PacketTimeoutTimestamp = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.PacketTimeoutTimestamp |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 7: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field PacketTimeoutHeight", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.PacketTimeoutHeight = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 8: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field PacketData", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthGenesis + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthGenesis + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.PacketData = append(m.PacketData[:0], dAtA[iNdEx:postIndex]...) + if m.PacketData == nil { + m.PacketData = []byte{} + } + iNdEx = postIndex + case 9: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field RefundSequence", wireType) + } + m.RefundSequence = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.RefundSequence |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 10: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field RetriesRemaining", wireType) + } + m.RetriesRemaining = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.RetriesRemaining |= int32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 11: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Timeout", wireType) + } + m.Timeout = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Timeout |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 12: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Nonrefundable", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenesis + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Nonrefundable = bool(v != 0) + default: + iNdEx = preIndex + skippy, err := skipGenesis(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthGenesis + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipGenesis(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGenesis + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGenesis + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowGenesis + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthGenesis + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupGenesis + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthGenesis + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthGenesis = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowGenesis = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupGenesis = fmt.Errorf("proto: unexpected end of group") +) diff --git a/modules/apps/packet-forward-middleware/types/keys.go b/modules/apps/packet-forward-middleware/types/keys.go new file mode 100644 index 00000000000..5b6d7b32233 --- /dev/null +++ b/modules/apps/packet-forward-middleware/types/keys.go @@ -0,0 +1,22 @@ +package types + +import "fmt" + +const ( + // ModuleName defines the module name + // NOTE: There is a spelling mistake in the module name that came from the original implementation + // and is currently kept for backward compatibility. Consideration for renaming done in #8388 + ModuleName = "packetfowardmiddleware" + + // StoreKey is the store key string for IBC transfer + StoreKey = ModuleName + + // QuerierRoute is the querier route for IBC transfer + QuerierRoute = ModuleName +) + +type NonrefundableKey struct{} + +func RefundPacketKey(channelID, portID string, sequence uint64) []byte { + return fmt.Appendf(nil, "%s/%s/%d", channelID, portID, sequence) +} diff --git a/modules/apps/packet-forward-middleware/types/types.go b/modules/apps/packet-forward-middleware/types/types.go new file mode 100644 index 00000000000..a63a6049d4a --- /dev/null +++ b/modules/apps/packet-forward-middleware/types/types.go @@ -0,0 +1,19 @@ +package types + +import ( + clienttypes "github.com/cosmos/ibc-go/v10/modules/core/02-client/types" + channeltypes "github.com/cosmos/ibc-go/v10/modules/core/04-channel/types" +) + +func (ifp *InFlightPacket) ChannelPacket() channeltypes.Packet { + return channeltypes.Packet{ + Data: ifp.PacketData, + Sequence: ifp.RefundSequence, + SourcePort: ifp.PacketSrcPortId, + SourceChannel: ifp.PacketSrcChannelId, + DestinationPort: ifp.RefundPortId, + DestinationChannel: ifp.RefundChannelId, + TimeoutHeight: clienttypes.MustParseHeight(ifp.PacketTimeoutHeight), + TimeoutTimestamp: ifp.PacketTimeoutTimestamp, + } +} diff --git a/modules/apps/transfer/keeper/keeper_test.go b/modules/apps/transfer/keeper/keeper_test.go index c961c755227..40a30c59e46 100644 --- a/modules/apps/transfer/keeper/keeper_test.go +++ b/modules/apps/transfer/keeper/keeper_test.go @@ -17,9 +17,9 @@ import ( authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + packetforwardkeeper "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/keeper" "github.com/cosmos/ibc-go/v10/modules/apps/transfer/keeper" "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" - channelkeeper "github.com/cosmos/ibc-go/v10/modules/core/04-channel/keeper" ibctesting "github.com/cosmos/ibc-go/v10/testing" ) @@ -338,12 +338,12 @@ func (suite *KeeperTestSuite) TestUnsetParams() { func (suite *KeeperTestSuite) TestWithICS4Wrapper() { suite.SetupTest() - // test if the ics4 wrapper is the channel keeper initially + // test if the ics4 wrapper is the pfm keeper initially ics4Wrapper := suite.chainA.GetSimApp().TransferKeeper.GetICS4Wrapper() - _, isChannelKeeper := ics4Wrapper.(*channelkeeper.Keeper) - suite.Require().True(isChannelKeeper) - suite.Require().IsType((*channelkeeper.Keeper)(nil), ics4Wrapper) + _, isPFMKeeper := ics4Wrapper.(*packetforwardkeeper.Keeper) + suite.Require().True(isPFMKeeper) + suite.Require().IsType((*packetforwardkeeper.Keeper)(nil), ics4Wrapper) // set the ics4 wrapper to the channel keeper suite.chainA.GetSimApp().TransferKeeper.WithICS4Wrapper(nil) diff --git a/modules/core/05-port/types/module.go b/modules/core/05-port/types/module.go index 2d2779ad126..28cfcc9447d 100644 --- a/modules/core/05-port/types/module.go +++ b/modules/core/05-port/types/module.go @@ -145,3 +145,8 @@ type PacketDataUnmarshaler interface { // the packet data can be unmarshaled based on the channel version. UnmarshalPacketData(ctx sdk.Context, portID string, channelID string, bz []byte) (any, string, error) } + +type PacketUnmarshalarModule interface { + PacketDataUnmarshaler + IBCModule +} diff --git a/modules/core/api/module.go b/modules/core/api/module.go index b102f35e221..cbc22af8fb1 100644 --- a/modules/core/api/module.go +++ b/modules/core/api/module.go @@ -69,3 +69,10 @@ type PacketDataUnmarshaler interface { // the payload is provided and the packet data interface is returned UnmarshalPacketData(payload channeltypesv2.Payload) (any, error) } + +// PacketUnmarshalarModuleV2 is an interface that combines the IBCModuleV2 and PacketDataUnmarshaler +// interfaces to assert that the underlying application supports both. +type PacketUnmarshalarModuleV2 interface { + IBCModule + PacketDataUnmarshaler +} diff --git a/modules/light-clients/08-wasm/go.mod b/modules/light-clients/08-wasm/go.mod index 46222b5dada..4c3f652ffc9 100644 --- a/modules/light-clients/08-wasm/go.mod +++ b/modules/light-clients/08-wasm/go.mod @@ -2,9 +2,10 @@ module github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10 go 1.23.8 -replace github.com/cosmos/ibc-go/v10 => ../../../ - -replace github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 +replace ( + github.com/cosmos/ibc-go/v10 => ../../../ + github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 +) require ( cosmossdk.io/api v0.9.2 @@ -144,6 +145,7 @@ require ( github.com/herumi/bls-eth-go-binary v1.31.0 // indirect github.com/holiman/uint256 v1.3.2 // indirect github.com/huandu/skiplist v1.2.1 // indirect + github.com/iancoleman/orderedmap v0.3.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/improbable-eng/grpc-web v0.15.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/modules/light-clients/08-wasm/go.sum b/modules/light-clients/08-wasm/go.sum index 1e16ced78bd..a23de7213f6 100644 --- a/modules/light-clients/08-wasm/go.sum +++ b/modules/light-clients/08-wasm/go.sum @@ -1223,6 +1223,8 @@ github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0Jr github.com/huandu/skiplist v1.2.1 h1:dTi93MgjwErA/8idWTzIw4Y1kZsMWx35fmI2c8Rij7w= github.com/huandu/skiplist v1.2.1/go.mod h1:7v3iFjLcSAzO4fN5B8dvebvo/qsfumiLiDXMrPiHF9w= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= diff --git a/proto/ibc/applications/packet_forward_middleware/v1/genesis.proto b/proto/ibc/applications/packet_forward_middleware/v1/genesis.proto new file mode 100644 index 00000000000..57af0b1a9d3 --- /dev/null +++ b/proto/ibc/applications/packet_forward_middleware/v1/genesis.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +package ibc.applications.packet_forward_middleware.v1; + +import "gogoproto/gogo.proto"; + +option go_package = "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/types"; + +// GenesisState defines the packetforward genesis state +message GenesisState { + // key - information about forwarded packet: src_channel + // (parsedReceiver.Channel), src_port (parsedReceiver.Port), sequence value - + // information about original packet for refunding if necessary: retries, + // srcPacketSender, srcPacket.DestinationChannel, srcPacket.DestinationPort + map in_flight_packets = 2 + [(gogoproto.moretags) = "yaml:\"in_flight_packets\"", (gogoproto.nullable) = false]; +} + +// InFlightPacket contains information about original packet for +// writing the acknowledgement and refunding if necessary. +message InFlightPacket { + string original_sender_address = 1; + string refund_channel_id = 2; + string refund_port_id = 3; + string packet_src_channel_id = 4; + string packet_src_port_id = 5; + uint64 packet_timeout_timestamp = 6; + string packet_timeout_height = 7; + bytes packet_data = 8; + uint64 refund_sequence = 9; + int32 retries_remaining = 10; + uint64 timeout = 11; + bool nonrefundable = 12; +} diff --git a/proto/ibc/applications/transfer/v1/token.proto b/proto/ibc/applications/transfer/v1/token.proto index 7a4dd889df5..cf8ac485b80 100644 --- a/proto/ibc/applications/transfer/v1/token.proto +++ b/proto/ibc/applications/transfer/v1/token.proto @@ -2,10 +2,10 @@ syntax = "proto3"; package ibc.applications.transfer.v1; -option go_package = "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types"; - import "gogoproto/gogo.proto"; +option go_package = "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types"; + // Token defines a struct which represents a token to be transferred. message Token { // the token denomination diff --git a/simapp/app.go b/simapp/app.go index a38a0ca0ae0..783aff8cb28 100644 --- a/simapp/app.go +++ b/simapp/app.go @@ -110,6 +110,9 @@ import ( icahostkeeper "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/host/keeper" icahosttypes "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/host/types" icatypes "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/types" + packetforward "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware" + packetforwardkeeper "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/keeper" + packetforwardtypes "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/types" "github.com/cosmos/ibc-go/v10/modules/apps/transfer" ibctransferkeeper "github.com/cosmos/ibc-go/v10/modules/apps/transfer/keeper" ibctransfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" @@ -181,6 +184,7 @@ type SimApp struct { GroupKeeper groupkeeper.Keeper ConsensusParamsKeeper consensusparamkeeper.Keeper CircuitKeeper circuitkeeper.Keeper + PFMKeeper *packetforwardkeeper.Keeper // the module manager ModuleManager *module.Manager @@ -266,7 +270,7 @@ func NewSimApp( minttypes.StoreKey, distrtypes.StoreKey, slashingtypes.StoreKey, govtypes.StoreKey, group.StoreKey, paramstypes.StoreKey, ibcexported.StoreKey, upgradetypes.StoreKey, feegrant.StoreKey, evidencetypes.StoreKey, ibctransfertypes.StoreKey, icacontrollertypes.StoreKey, icahosttypes.StoreKey, - authzkeeper.StoreKey, consensusparamtypes.StoreKey, circuittypes.StoreKey, + authzkeeper.StoreKey, consensusparamtypes.StoreKey, circuittypes.StoreKey, packetforwardtypes.StoreKey, ) // register streaming services @@ -387,11 +391,17 @@ func NewSimApp( authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) + // Packet Forward Middleware keeper + app.PFMKeeper = packetforwardkeeper.NewKeeper(appCodec, runtime.NewKVStoreService(keys[packetforwardtypes.StoreKey]), app.TransferKeeper, app.IBCKeeper.ChannelKeeper, app.BankKeeper, app.ICAControllerKeeper.GetICS4Wrapper(), authtypes.NewModuleAddress(govtypes.ModuleName).String()) + // Create IBC Router ibcRouter := porttypes.NewRouter() // Middleware Stacks + // PacketForwardMiddleware must be created before TransferKeeper + app.PFMKeeper = packetforwardkeeper.NewKeeper(appCodec, runtime.NewKVStoreService(keys[packetforwardtypes.StoreKey]), nil, app.IBCKeeper.ChannelKeeper, app.BankKeeper, app.ICAControllerKeeper.GetICS4Wrapper(), authtypes.NewModuleAddress(govtypes.ModuleName).String()) + // Create Transfer Keeper app.TransferKeeper = ibctransferkeeper.NewKeeper( appCodec, runtime.NewKVStoreService(keys[ibctransfertypes.StoreKey]), app.GetSubspace(ibctransfertypes.ModuleName), @@ -402,6 +412,8 @@ func NewSimApp( authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) + app.PFMKeeper.SetTransferKeeper(app.TransferKeeper) + // Create Transfer Stack // SendPacket, since it is originating from the application to core IBC: // transferKeeper.SendPacket -> channel.SendPacket @@ -413,11 +425,14 @@ func NewSimApp( // - Transfer // create IBC module from bottom to top of stack - var transferStack porttypes.IBCModule = transfer.NewIBCModule(app.TransferKeeper) + transferStack := packetforward.NewIBCMiddleware(transfer.NewIBCModule(app.TransferKeeper), app.PFMKeeper, 0, packetforwardkeeper.DefaultForwardTransferPacketTimeoutTimestamp) + app.TransferKeeper.WithICS4Wrapper(app.PFMKeeper) // Add transfer stack to IBC Router ibcRouter.AddRoute(ibctransfertypes.ModuleName, transferStack) + // Packet Forward Middleware Stack. + // Create Interchain Accounts Stack // SendPacket, since it is originating from the application to core IBC: // icaControllerKeeper.SendTx -> channel.SendPacket @@ -487,6 +502,7 @@ func NewSimApp( ibc.NewAppModule(app.IBCKeeper), transfer.NewAppModule(app.TransferKeeper), ica.NewAppModule(&app.ICAControllerKeeper, &app.ICAHostKeeper), + packetforward.NewAppModule(app.PFMKeeper), // IBC light clients ibctm.NewAppModule(tmLightClientModule), @@ -527,6 +543,7 @@ func NewSimApp( evidencetypes.ModuleName, stakingtypes.ModuleName, ibcexported.ModuleName, + packetforwardtypes.ModuleName, ibctransfertypes.ModuleName, genutiltypes.ModuleName, authz.ModuleName, @@ -537,6 +554,7 @@ func NewSimApp( govtypes.ModuleName, stakingtypes.ModuleName, ibcexported.ModuleName, + packetforwardtypes.ModuleName, ibctransfertypes.ModuleName, genutiltypes.ModuleName, feegrant.ModuleName, @@ -552,7 +570,7 @@ func NewSimApp( banktypes.ModuleName, distrtypes.ModuleName, stakingtypes.ModuleName, slashingtypes.ModuleName, govtypes.ModuleName, minttypes.ModuleName, crisistypes.ModuleName, ibcexported.ModuleName, genutiltypes.ModuleName, evidencetypes.ModuleName, authz.ModuleName, ibctransfertypes.ModuleName, - icatypes.ModuleName, feegrant.ModuleName, paramstypes.ModuleName, upgradetypes.ModuleName, + packetforwardtypes.ModuleName, icatypes.ModuleName, feegrant.ModuleName, paramstypes.ModuleName, upgradetypes.ModuleName, vestingtypes.ModuleName, group.ModuleName, consensusparamtypes.ModuleName, circuittypes.ModuleName, } app.ModuleManager.SetOrderInitGenesis(genesisModuleOrder...) diff --git a/simapp/go.mod b/simapp/go.mod index 0b12feb8110..663c6f879ba 100644 --- a/simapp/go.mod +++ b/simapp/go.mod @@ -140,6 +140,7 @@ require ( github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/holiman/uint256 v1.3.2 // indirect github.com/huandu/skiplist v1.2.1 // indirect + github.com/iancoleman/orderedmap v0.3.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/improbable-eng/grpc-web v0.15.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/simapp/go.sum b/simapp/go.sum index 6f11dd64775..5e8c72f08c9 100644 --- a/simapp/go.sum +++ b/simapp/go.sum @@ -1199,6 +1199,8 @@ github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0Jr github.com/huandu/skiplist v1.2.1 h1:dTi93MgjwErA/8idWTzIw4Y1kZsMWx35fmI2c8Rij7w= github.com/huandu/skiplist v1.2.1/go.mod h1:7v3iFjLcSAzO4fN5B8dvebvo/qsfumiLiDXMrPiHF9w= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= diff --git a/testing/simapp/app.go b/testing/simapp/app.go index ae21ddb29ac..3ea0d571add 100644 --- a/testing/simapp/app.go +++ b/testing/simapp/app.go @@ -91,7 +91,10 @@ import ( icahostkeeper "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/host/keeper" icahosttypes "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/host/types" icatypes "github.com/cosmos/ibc-go/v10/modules/apps/27-interchain-accounts/types" - "github.com/cosmos/ibc-go/v10/modules/apps/transfer" + packetforward "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware" + packetforwardkeeper "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/keeper" + packetforwardtypes "github.com/cosmos/ibc-go/v10/modules/apps/packet-forward-middleware/types" + transfer "github.com/cosmos/ibc-go/v10/modules/apps/transfer" ibctransferkeeper "github.com/cosmos/ibc-go/v10/modules/apps/transfer/keeper" ibctransfertypes "github.com/cosmos/ibc-go/v10/modules/apps/transfer/types" transferv2 "github.com/cosmos/ibc-go/v10/modules/apps/transfer/v2" @@ -160,6 +163,7 @@ type SimApp struct { ICAHostKeeper icahostkeeper.Keeper TransferKeeper ibctransferkeeper.Keeper ConsensusParamsKeeper consensusparamkeeper.Keeper + PFMKeeper *packetforwardkeeper.Keeper // make IBC modules public for test purposes // these modules are never directly routed to by the IBC Router @@ -252,7 +256,7 @@ func NewSimApp( authtypes.StoreKey, banktypes.StoreKey, stakingtypes.StoreKey, minttypes.StoreKey, distrtypes.StoreKey, slashingtypes.StoreKey, govtypes.StoreKey, group.StoreKey, paramstypes.StoreKey, ibcexported.StoreKey, upgradetypes.StoreKey, - ibctransfertypes.StoreKey, icacontrollertypes.StoreKey, icahosttypes.StoreKey, + packetforwardtypes.StoreKey, ibctransfertypes.StoreKey, icacontrollertypes.StoreKey, icahosttypes.StoreKey, authzkeeper.StoreKey, consensusparamtypes.StoreKey, ) @@ -366,6 +370,9 @@ func NewSimApp( // Middleware Stacks + // PacketForwardMiddleware must be created before TransferKeeper + app.PFMKeeper = packetforwardkeeper.NewKeeper(appCodec, runtime.NewKVStoreService(keys[packetforwardtypes.StoreKey]), nil, app.IBCKeeper.ChannelKeeper, app.BankKeeper, app.ICAControllerKeeper.GetICS4Wrapper(), authtypes.NewModuleAddress(govtypes.ModuleName).String()) + // Create Transfer Keeper app.TransferKeeper = ibctransferkeeper.NewKeeper( appCodec, runtime.NewKVStoreService(keys[ibctransfertypes.StoreKey]), app.GetSubspace(ibctransfertypes.ModuleName), @@ -376,6 +383,8 @@ func NewSimApp( authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) + app.PFMKeeper.SetTransferKeeper(app.TransferKeeper) + // Mock Module Stack // Mock Module setup for testing IBC and also acts as the interchain accounts authentication module @@ -403,7 +412,8 @@ func NewSimApp( // channel.RecvPacket -> transfer.OnRecvPacket // create IBC module from bottom to top of stack - var transferStack porttypes.IBCModule = transfer.NewIBCModule(app.TransferKeeper) + transferStack := packetforward.NewIBCMiddleware(transfer.NewIBCModule(app.TransferKeeper), app.PFMKeeper, 0, packetforwardkeeper.DefaultForwardTransferPacketTimeoutTimestamp) + app.TransferKeeper.WithICS4Wrapper(app.PFMKeeper) // Add transfer stack to IBC Router ibcRouter.AddRoute(ibctransfertypes.ModuleName, transferStack) @@ -485,6 +495,7 @@ func NewSimApp( transfer.NewAppModule(app.TransferKeeper), ica.NewAppModule(&app.ICAControllerKeeper, &app.ICAHostKeeper), mockModule, + packetforward.NewAppModule(app.PFMKeeper), // IBC light clients ibctm.NewAppModule(tmLightClientModule), @@ -525,6 +536,7 @@ func NewSimApp( stakingtypes.ModuleName, ibcexported.ModuleName, ibctransfertypes.ModuleName, + packetforwardtypes.ModuleName, genutiltypes.ModuleName, authz.ModuleName, icatypes.ModuleName, @@ -535,6 +547,7 @@ func NewSimApp( stakingtypes.ModuleName, ibcexported.ModuleName, ibctransfertypes.ModuleName, + packetforwardtypes.ModuleName, genutiltypes.ModuleName, icatypes.ModuleName, ibcmock.ModuleName, @@ -549,7 +562,7 @@ func NewSimApp( banktypes.ModuleName, distrtypes.ModuleName, stakingtypes.ModuleName, slashingtypes.ModuleName, govtypes.ModuleName, minttypes.ModuleName, ibcexported.ModuleName, genutiltypes.ModuleName, authz.ModuleName, ibctransfertypes.ModuleName, - icatypes.ModuleName, ibcmock.ModuleName, paramstypes.ModuleName, upgradetypes.ModuleName, + packetforwardtypes.ModuleName, icatypes.ModuleName, ibcmock.ModuleName, paramstypes.ModuleName, upgradetypes.ModuleName, vestingtypes.ModuleName, group.ModuleName, consensusparamtypes.ModuleName, } app.ModuleManager.SetOrderInitGenesis(genesisModuleOrder...)