Skip to content

Commit

Permalink
feat: bumpup ibc-go to v4 (#61)
Browse files Browse the repository at this point in the history
* feat: Upgrade to IBC v4.2.0 with interchain-accounts v0.2.4 (CosmWasm#1088)

cherry-pick original CosmWasm#1088 except ICA part.

* Upgrade to IBC v4.2.0 with interchain-accounts v0.2.4

* Fix linter issues

* Test version handshake

* Add more ibc e2e scenarios

* Add fee middleware to wasm

* Get ibc app version from middleware

* feat: remove channel hack

cherry-picking d522e54

* docs: add this PR to CHANGELOG

* chore: execute go fmt

* fix: add missed e2e ibc-fees test in cherry-pick

* chore: make format

* fix: fix missed duplicating changes for app/app.go to appplus/app.go

* build: upgrade finschia-sdk to v0.48.0-rc1

---------

Co-authored-by: Alexander Peters <alpe@users.noreply.github.com>
  • Loading branch information
loloicci and alpe authored Aug 3, 2023
1 parent 164efa4 commit dedcd9e
Show file tree
Hide file tree
Showing 39 changed files with 1,124 additions and 489 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [Unreleased](https://github.com/Finschia/wasmd/compare/v0.1.4...HEAD)

### Features
* [\#61](https://github.com/Finschia/wasmd/pull/61) bumpup ibc-go to v4

### Improvements

Expand All @@ -24,7 +25,7 @@
* [\#46](https://github.com/Finschia/wasmd/pull/46) add admin-related events

### Improvements
* [\#43](https://github.com/Finschia/wasmd/pull/43) delete unnecessary test
* [\#43](https://github.com/Finschia/wasmd/pull/43) delete unnecessary test

### Bug Fixes
* [\#35](https://github.com/Finschia/wasmd/pull/35) stop wrap twice the response of handling non-plus wasm message in plus handler
Expand Down
4 changes: 2 additions & 2 deletions app/ante.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
sdk "github.com/Finschia/finschia-sdk/types"
sdkerrors "github.com/Finschia/finschia-sdk/types/errors"
"github.com/Finschia/finschia-sdk/x/auth/ante"
ibcante "github.com/Finschia/ibc-go/v3/modules/core/ante"
"github.com/Finschia/ibc-go/v3/modules/core/keeper"
ibcante "github.com/cosmos/ibc-go/v4/modules/core/ante"
"github.com/cosmos/ibc-go/v4/modules/core/keeper"

wasmkeeper "github.com/Finschia/wasmd/x/wasm/keeper"
wasmTypes "github.com/Finschia/wasmd/x/wasm/types"
Expand Down
119 changes: 83 additions & 36 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,27 @@ import (
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
dbm "github.com/tendermint/tm-db"

ica "github.com/Finschia/ibc-go/v3/modules/apps/27-interchain-accounts"
icacontrollerkeeper "github.com/Finschia/ibc-go/v3/modules/apps/27-interchain-accounts/controller/keeper"
icacontrollertypes "github.com/Finschia/ibc-go/v3/modules/apps/27-interchain-accounts/controller/types"
icahost "github.com/Finschia/ibc-go/v3/modules/apps/27-interchain-accounts/host"
icahostkeeper "github.com/Finschia/ibc-go/v3/modules/apps/27-interchain-accounts/host/keeper"
icahosttypes "github.com/Finschia/ibc-go/v3/modules/apps/27-interchain-accounts/host/types"
icatypes "github.com/Finschia/ibc-go/v3/modules/apps/27-interchain-accounts/types"
transfer "github.com/Finschia/ibc-go/v3/modules/apps/transfer"
ibctransferkeeper "github.com/Finschia/ibc-go/v3/modules/apps/transfer/keeper"
ibctransfertypes "github.com/Finschia/ibc-go/v3/modules/apps/transfer/types"
ibc "github.com/Finschia/ibc-go/v3/modules/core"
ibcclient "github.com/Finschia/ibc-go/v3/modules/core/02-client"
ibcclientclient "github.com/Finschia/ibc-go/v3/modules/core/02-client/client"
ibcclienttypes "github.com/Finschia/ibc-go/v3/modules/core/02-client/types"
porttypes "github.com/Finschia/ibc-go/v3/modules/core/05-port/types"
ibchost "github.com/Finschia/ibc-go/v3/modules/core/24-host"
ibckeeper "github.com/Finschia/ibc-go/v3/modules/core/keeper"
ocabci "github.com/Finschia/ostracon/abci/types"
ica "github.com/cosmos/ibc-go/v4/modules/apps/27-interchain-accounts"
icacontrollerkeeper "github.com/cosmos/ibc-go/v4/modules/apps/27-interchain-accounts/controller/keeper"
icacontrollertypes "github.com/cosmos/ibc-go/v4/modules/apps/27-interchain-accounts/controller/types"
icahost "github.com/cosmos/ibc-go/v4/modules/apps/27-interchain-accounts/host"
icahostkeeper "github.com/cosmos/ibc-go/v4/modules/apps/27-interchain-accounts/host/keeper"
icahosttypes "github.com/cosmos/ibc-go/v4/modules/apps/27-interchain-accounts/host/types"
icatypes "github.com/cosmos/ibc-go/v4/modules/apps/27-interchain-accounts/types"
ibcfee "github.com/cosmos/ibc-go/v4/modules/apps/29-fee"
ibcfeekeeper "github.com/cosmos/ibc-go/v4/modules/apps/29-fee/keeper"
ibcfeetypes "github.com/cosmos/ibc-go/v4/modules/apps/29-fee/types"
transfer "github.com/cosmos/ibc-go/v4/modules/apps/transfer"
ibctransferkeeper "github.com/cosmos/ibc-go/v4/modules/apps/transfer/keeper"
ibctransfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types"
ibc "github.com/cosmos/ibc-go/v4/modules/core"
ibcclient "github.com/cosmos/ibc-go/v4/modules/core/02-client"
ibcclientclient "github.com/cosmos/ibc-go/v4/modules/core/02-client/client"
ibcclienttypes "github.com/cosmos/ibc-go/v4/modules/core/02-client/types"
porttypes "github.com/cosmos/ibc-go/v4/modules/core/05-port/types"
ibchost "github.com/cosmos/ibc-go/v4/modules/core/24-host"
ibckeeper "github.com/cosmos/ibc-go/v4/modules/core/keeper"

"github.com/Finschia/finschia-sdk/baseapp"
"github.com/Finschia/finschia-sdk/client"
Expand Down Expand Up @@ -203,6 +206,7 @@ var (
vesting.AppModuleBasic{},
wasm.AppModuleBasic{},
ica.AppModuleBasic{},
ibcfee.AppModuleBasic{},
)

// module account permissions
Expand All @@ -214,6 +218,7 @@ var (
stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking},
govtypes.ModuleName: {authtypes.Burner},
ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner},
ibcfeetypes.ModuleName: nil,
icatypes.ModuleName: nil,
wasm.ModuleName: {authtypes.Burner},
}
Expand Down Expand Up @@ -259,6 +264,7 @@ type WasmApp struct {
ParamsKeeper paramskeeper.Keeper
EvidenceKeeper evidencekeeper.Keeper
IBCKeeper *ibckeeper.Keeper // IBC Keeper must be a pointer in the app, so we can SetRouter on it correctly
IBCFeeKeeper ibcfeekeeper.Keeper
ICAControllerKeeper icacontrollerkeeper.Keeper
ICAHostKeeper icahostkeeper.Keeper
TransferKeeper ibctransferkeeper.Keeper
Expand All @@ -270,6 +276,7 @@ type WasmApp struct {
ScopedICAHostKeeper capabilitykeeper.ScopedKeeper
ScopedICAControllerKeeper capabilitykeeper.ScopedKeeper
ScopedTransferKeeper capabilitykeeper.ScopedKeeper
ScopedIBCFeeKeeper capabilitykeeper.ScopedKeeper
ScopedWasmKeeper capabilitykeeper.ScopedKeeper

// make IBC modules public for test purposes
Expand Down Expand Up @@ -313,7 +320,7 @@ func NewWasmApp(
minttypes.StoreKey, distrtypes.StoreKey, slashingtypes.StoreKey,
govtypes.StoreKey, paramstypes.StoreKey, ibchost.StoreKey, upgradetypes.StoreKey,
evidencetypes.StoreKey, ibctransfertypes.StoreKey, capabilitytypes.StoreKey,
feegrant.StoreKey, authzkeeper.StoreKey, wasm.StoreKey, icahosttypes.StoreKey, icacontrollertypes.StoreKey,
feegrant.StoreKey, authzkeeper.StoreKey, wasm.StoreKey, icahosttypes.StoreKey, icacontrollertypes.StoreKey, ibcfeetypes.StoreKey,
)
tkeys := sdk.NewTransientStoreKeys(paramstypes.TStoreKey)
memKeys := sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey)
Expand Down Expand Up @@ -448,20 +455,26 @@ func NewWasmApp(
AddRoute(upgradetypes.RouterKey, upgrade.NewSoftwareUpgradeProposalHandler(app.UpgradeKeeper)).
AddRoute(ibcclienttypes.RouterKey, ibcclient.NewClientProposalHandler(app.IBCKeeper.ClientKeeper))

// IBC Fee Module keeper
app.IBCFeeKeeper = ibcfeekeeper.NewKeeper(
appCodec, keys[ibcfeetypes.StoreKey], app.getSubspace(ibcfeetypes.ModuleName),
app.IBCKeeper.ChannelKeeper, // may be replaced with IBC middleware
app.IBCKeeper.ChannelKeeper,
&app.IBCKeeper.PortKeeper, app.AccountKeeper, app.BankKeeper,
)

// Create Transfer Keepers
app.TransferKeeper = ibctransferkeeper.NewKeeper(
appCodec,
keys[ibctransfertypes.StoreKey],
app.getSubspace(ibctransfertypes.ModuleName),
app.IBCKeeper.ChannelKeeper,
app.IBCFeeKeeper, // ISC4 Wrapper: fee IBC middleware
app.IBCKeeper.ChannelKeeper,
&app.IBCKeeper.PortKeeper,
app.AccountKeeper,
app.BankKeeper,
scopedTransferKeeper,
)
transferModule := transfer.NewAppModule(app.TransferKeeper)
transferIBCModule := transfer.NewIBCModule(app.TransferKeeper)

app.ICAHostKeeper = icahostkeeper.NewKeeper(
appCodec,
Expand All @@ -477,14 +490,12 @@ func NewWasmApp(
appCodec,
keys[icacontrollertypes.StoreKey],
app.getSubspace(icacontrollertypes.SubModuleName),
app.IBCKeeper.ChannelKeeper, // may be replaced with middleware such as ics29 fee
app.IBCFeeKeeper, // use ics29 fee as ics4Wrapper in middleware stack
app.IBCKeeper.ChannelKeeper,
&app.IBCKeeper.PortKeeper,
scopedICAControllerKeeper,
app.MsgServiceRouter(),
)
icaModule := ica.NewAppModule(&app.ICAControllerKeeper, &app.ICAHostKeeper)
icaHostIBCModule := icahost.NewIBCModule(app.ICAHostKeeper)

// create evidence keeper with router
evidenceKeeper := evidencekeeper.NewKeeper(
Expand Down Expand Up @@ -524,19 +535,49 @@ func NewWasmApp(
wasmOpts...,
)

// Create static IBC router, add app routes, then set and seal it
ibcRouter := porttypes.NewRouter()

// The gov proposal types can be individually enabled
if len(enabledProposals) != 0 {
govRouter.AddRoute(wasm.RouterKey, wasm.NewWasmProposalHandler(app.WasmKeeper, enabledProposals))
}
ibcRouter.
AddRoute(wasm.ModuleName, wasm.NewIBCHandler(app.WasmKeeper, app.IBCKeeper.ChannelKeeper)).
AddRoute(ibctransfertypes.ModuleName, transferIBCModule).
// AddRoute(icacontrollertypes.SubModuleName, icaControllerIBCModule).
AddRoute(icahosttypes.SubModuleName, icaHostIBCModule)
// AddRoute(intertxtypes.ModuleName, icaControllerIBCModule)

// Create Transfer Stack
var transferStack porttypes.IBCModule
transferStack = transfer.NewIBCModule(app.TransferKeeper)
transferStack = ibcfee.NewIBCMiddleware(transferStack, app.IBCFeeKeeper)

/*
// Create Interchain Accounts Stack
// SendPacket, since it is originating from the application to core IBC:
// icaAuthModuleKeeper.SendTx -> icaController.SendPacket -> fee.SendPacket -> channel.SendPacket
// Note: please do your research before using this in production app, this is a demo and not an officially
// supported IBC team implementation. Do your own research before using it.
var icaControllerStack porttypes.IBCModule
// You will likely want to use your own reviewed and maintained ica auth module
icaControllerStack = intertx.NewIBCModule(app.InterTxKeeper)
icaControllerStack = icacontroller.NewIBCMiddleware(icaControllerStack, app.ICAControllerKeeper)
icaControllerStack = ibcfee.NewIBCMiddleware(icaControllerStack, app.IBCFeeKeeper)
*/

// RecvPacket, message that originates from core IBC and goes down to app, the flow is:
// channel.RecvPacket -> fee.OnRecvPacket -> icaHost.OnRecvPacket
var icaHostStack porttypes.IBCModule
icaHostStack = icahost.NewIBCModule(app.ICAHostKeeper)
icaHostStack = ibcfee.NewIBCMiddleware(icaHostStack, app.IBCFeeKeeper)

// Create fee enabled wasm ibc Stack
var wasmStack porttypes.IBCModule
wasmStack = wasm.NewIBCHandler(app.WasmKeeper, app.IBCKeeper.ChannelKeeper, app.IBCFeeKeeper)
wasmStack = ibcfee.NewIBCMiddleware(wasmStack, app.IBCFeeKeeper)

// Create static IBC router, add app routes, then set and seal it
ibcRouter := porttypes.NewRouter().
AddRoute(ibctransfertypes.ModuleName, transferStack).
AddRoute(wasm.ModuleName, wasmStack).
// AddRoute(intertxtypes.ModuleName, icaControllerStack).
// AddRoute(icacontrollertypes.SubModuleName, icaControllerStack).
AddRoute(icahosttypes.SubModuleName, icaHostStack)

app.IBCKeeper.SetRouter(ibcRouter)

app.GovKeeper = govkeeper.NewKeeper(
Expand Down Expand Up @@ -579,8 +620,10 @@ func NewWasmApp(
authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry),
ibc.NewAppModule(app.IBCKeeper),
params.NewAppModule(app.ParamsKeeper),
transferModule,
icaModule,
transfer.NewAppModule(app.TransferKeeper),
ibcfee.NewAppModule(app.IBCFeeKeeper),
ica.NewAppModule(&app.ICAControllerKeeper, &app.ICAHostKeeper),
// intertx.NewAppModule(appCodec, app.InterTxKeeper),
crisis.NewAppModule(&app.CrisisKeeper, skipGenesisInvariants), // always be last to make sure that it checks for all invariants and not only part of them
)

Expand Down Expand Up @@ -609,6 +652,7 @@ func NewWasmApp(
ibctransfertypes.ModuleName,
ibchost.ModuleName,
icatypes.ModuleName,
ibcfeetypes.ModuleName,
// intertxtypes.ModuleName,
wasm.ModuleName,
)
Expand All @@ -634,6 +678,7 @@ func NewWasmApp(
ibctransfertypes.ModuleName,
ibchost.ModuleName,
icatypes.ModuleName,
ibcfeetypes.ModuleName,
// intertxtypes.ModuleName,
wasm.ModuleName,
)
Expand Down Expand Up @@ -666,6 +711,8 @@ func NewWasmApp(
ibctransfertypes.ModuleName,
ibchost.ModuleName,
icatypes.ModuleName,
ibcfeetypes.ModuleName,
// intertxtypes.ModuleName,
// wasm after ibc transfer
wasm.ModuleName,
)
Expand Down Expand Up @@ -698,7 +745,7 @@ func NewWasmApp(
evidence.NewAppModule(app.EvidenceKeeper),
wasm.NewAppModule(appCodec, &app.WasmKeeper, app.StakingKeeper, app.AccountKeeper, app.BankKeeper),
ibc.NewAppModule(app.IBCKeeper),
transferModule,
transfer.NewAppModule(app.TransferKeeper),
)

app.sm.RegisterStoreDecoders()
Expand Down
4 changes: 2 additions & 2 deletions app/sim_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ import (
"github.com/Finschia/finschia-sdk/x/simulation"
slashingtypes "github.com/Finschia/finschia-sdk/x/slashing/types"
stakingtypes "github.com/Finschia/finschia-sdk/x/staking/types"
ibctransfertypes "github.com/Finschia/ibc-go/v3/modules/apps/transfer/types"
ibchost "github.com/Finschia/ibc-go/v3/modules/core/24-host"
"github.com/Finschia/ostracon/libs/log"
ibctransfertypes "github.com/cosmos/ibc-go/v4/modules/apps/transfer/types"
ibchost "github.com/cosmos/ibc-go/v4/modules/core/24-host"

"github.com/Finschia/wasmd/x/wasm"
wasmtypes "github.com/Finschia/wasmd/x/wasm/types"
Expand Down
4 changes: 2 additions & 2 deletions app/test_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (
bankkeeper "github.com/Finschia/finschia-sdk/x/bank/keeper"
capabilitykeeper "github.com/Finschia/finschia-sdk/x/capability/keeper"
stakingkeeper "github.com/Finschia/finschia-sdk/x/staking/keeper"
ibctransferkeeper "github.com/Finschia/ibc-go/v3/modules/apps/transfer/keeper"
ibckeeper "github.com/Finschia/ibc-go/v3/modules/core/keeper"
ibctransferkeeper "github.com/cosmos/ibc-go/v4/modules/apps/transfer/keeper"
ibckeeper "github.com/cosmos/ibc-go/v4/modules/core/keeper"

"github.com/Finschia/wasmd/app/params"
"github.com/Finschia/wasmd/x/wasm"
Expand Down
31 changes: 15 additions & 16 deletions app/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func Setup(isCheckTx bool, opts ...wasm.Option) *WasmApp {
// that also act as delegators. For simplicity, each validator is bonded with a delegation
// of one consensus engine unit (10^6) in the default token of the WasmApp from first genesis
// account. A Nop logger is set in WasmApp.
func SetupWithGenesisValSet(t *testing.T, valSet *tmtypes.ValidatorSet, genAccs []authtypes.GenesisAccount, opts []wasm.Option, balances ...banktypes.Balance) *WasmApp {
func SetupWithGenesisValSet(t *testing.T, valSet *tmtypes.ValidatorSet, genAccs []authtypes.GenesisAccount, chainID string, opts []wasm.Option, balances ...banktypes.Balance) *WasmApp {
app, genesisState := setup(t, true, 5, opts...)
// set genesis accounts
authGenesis := authtypes.NewGenesisState(authtypes.DefaultParams(), genAccs)
Expand All @@ -108,7 +108,7 @@ func SetupWithGenesisValSet(t *testing.T, valSet *tmtypes.ValidatorSet, genAccs
validators := make([]stakingtypes.Validator, 0, len(valSet.Validators))
delegations := make([]stakingtypes.Delegation, 0, len(valSet.Validators))

bondAmt := sdk.NewInt(1000000)
bondAmt := sdk.TokensFromConsensusPower(1, sdk.DefaultPowerReduction)

for _, val := range valSet.Validators {
pk, err := cryptocodec.FromOcPubKeyInterface(val.PubKey)
Expand All @@ -128,37 +128,38 @@ func SetupWithGenesisValSet(t *testing.T, valSet *tmtypes.ValidatorSet, genAccs
Commission: stakingtypes.NewCommission(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()),
MinSelfDelegation: sdk.ZeroInt(),
}

validators = append(validators, validator)
delegations = append(delegations, stakingtypes.NewDelegation(genAccs[0].GetAddress(), val.Address.Bytes(), sdk.OneDec()))

}

// set validators and delegations
stakingGenesis := stakingtypes.NewGenesisState(stakingtypes.DefaultParams(), validators, delegations)
genesisState[stakingtypes.ModuleName] = app.appCodec.MustMarshalJSON(stakingGenesis)
var stakingGenesis stakingtypes.GenesisState
app.AppCodec().MustUnmarshalJSON(genesisState[stakingtypes.ModuleName], &stakingGenesis)

totalSupply := sdk.NewCoins()
for _, b := range balances {
// add genesis acc tokens and delegated tokens to total supply
totalSupply = totalSupply.Add(b.Coins.Add(sdk.NewCoin(sdk.DefaultBondDenom, bondAmt))...)
}
bondDenom := stakingGenesis.Params.BondDenom

// add bonded amount to bonded pool module account
balances = append(balances, banktypes.Balance{
Address: authtypes.NewModuleAddress(stakingtypes.BondedPoolName).String(),
Coins: sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, bondAmt)},
Coins: sdk.Coins{sdk.NewCoin(bondDenom, bondAmt.Mul(sdk.NewInt(int64(len(valSet.Validators)))))},
})

// set validators and delegations
stakingGenesis = *stakingtypes.NewGenesisState(stakingGenesis.Params, validators, delegations)
genesisState[stakingtypes.ModuleName] = app.AppCodec().MustMarshalJSON(&stakingGenesis)

// update total supply
bankGenesis := banktypes.NewGenesisState(banktypes.DefaultGenesisState().Params, balances, totalSupply, []banktypes.Metadata{})
genesisState[banktypes.ModuleName] = app.appCodec.MustMarshalJSON(bankGenesis)
bankGenesis := banktypes.NewGenesisState(banktypes.DefaultGenesisState().Params, balances, sdk.NewCoins(), []banktypes.Metadata{})
genesisState[banktypes.ModuleName] = app.AppCodec().MustMarshalJSON(bankGenesis)

stateBytes, err := json.MarshalIndent(genesisState, "", " ")
require.NoError(t, err)

// init chain will set the validator set and initialize the genesis accounts
app.InitChain(
abci.RequestInitChain{
ChainId: chainID,
Validators: []abci.ValidatorUpdate{},
ConsensusParams: DefaultConsensusParams,
AppStateBytes: stateBytes,
Expand All @@ -168,6 +169,7 @@ func SetupWithGenesisValSet(t *testing.T, valSet *tmtypes.ValidatorSet, genAccs
// commit genesis changes
app.Commit()
app.BeginBlock(ocabci.RequestBeginBlock{Header: tmproto.Header{
ChainID: chainID,
Height: app.LastBlockHeight() + 1,
AppHash: app.LastCommitID().Hash,
ValidatorsHash: valSet.Hash(),
Expand Down Expand Up @@ -391,9 +393,6 @@ func SignAndDeliver(
require.Nil(t, res)
}

app.EndBlock(abci.RequestEndBlock{})
app.Commit()

return gInfo, res, err
}

Expand Down
Loading

0 comments on commit dedcd9e

Please sign in to comment.