package keeper_test import ( "encoding/json" "errors" "testing" wasmvm "github.com/CosmWasm/wasmvm/v2" wasmvmtypes "github.com/CosmWasm/wasmvm/v2/types" dbm "github.com/cosmos/cosmos-db" testifysuite "github.com/stretchr/testify/suite" "cosmossdk.io/log" storetypes "cosmossdk.io/store/types" "git.cw.tr/mukan-network/mukan-sdk/baseapp" "git.cw.tr/mukan-network/mukan-sdk/runtime" simtestutil "git.cw.tr/mukan-network/mukan-sdk/testutil/sims" authtypes "git.cw.tr/mukan-network/mukan-sdk/x/auth/types" govtypes "git.cw.tr/mukan-network/mukan-sdk/x/gov/types" "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10/keeper" wasmtesting "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10/testing" "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10/testing/simapp" "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10/types" clienttypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/02-client/types" host "git.cw.tr/mukan-network/mukan-ibc/modules/core/24-host" "git.cw.tr/mukan-network/mukan-ibc/modules/core/exported" ibctm "git.cw.tr/mukan-network/mukan-ibc/modules/light-clients/07-tendermint" ibctesting "git.cw.tr/mukan-network/mukan-ibc/testing" ) const ( defaultWasmClientID = "08-wasm-0" ) type KeeperTestSuite struct { testifysuite.Suite coordinator *ibctesting.Coordinator // mockVM is a mock wasm VM that implements the WasmEngine interface mockVM *wasmtesting.MockWasmEngine chainA *ibctesting.TestChain } // setupTestingApp provides the duplicated simapp which is specific to the 08-wasm module on chain creation. func setupTestingApp() (ibctesting.TestingApp, map[string]json.RawMessage) { db := dbm.NewMemDB() app := simapp.NewUnitTestSimApp(log.NewNopLogger(), db, nil, true, simtestutil.EmptyAppOptions{}, nil) return app, app.DefaultGenesis() } // GetSimApp returns the duplicated SimApp from within the 08-wasm directory. // This must be used instead of chain.GetSimApp() for tests within this directory. func GetSimApp(chain *ibctesting.TestChain) *simapp.SimApp { app, ok := chain.App.(*simapp.SimApp) if !ok { panic(errors.New("chain is not a simapp.SimApp")) } return app } func (suite *KeeperTestSuite) SetupTest() { suite.coordinator = ibctesting.NewCustomAppCoordinator(suite.T(), 1, setupTestingApp) suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(1)) queryHelper := baseapp.NewQueryServerTestHelper(suite.chainA.GetContext(), GetSimApp(suite.chainA).InterfaceRegistry()) types.RegisterQueryServer(queryHelper, GetSimApp(suite.chainA).WasmClientKeeper) } // SetupWasmWithMockVM sets up mock cometbft chain with a mock vm. func (suite *KeeperTestSuite) SetupWasmWithMockVM() { suite.coordinator = ibctesting.NewCustomAppCoordinator(suite.T(), 1, suite.setupWasmWithMockVM) suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(1)) } func (suite *KeeperTestSuite) setupWasmWithMockVM() (ibctesting.TestingApp, map[string]json.RawMessage) { suite.mockVM = wasmtesting.NewMockWasmEngine() suite.mockVM.InstantiateFn = func(checksum wasmvm.Checksum, env wasmvmtypes.Env, info wasmvmtypes.MessageInfo, initMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { var payload types.InstantiateMessage err := json.Unmarshal(initMsg, &payload) suite.Require().NoError(err) wrappedClientState, ok := clienttypes.MustUnmarshalClientState(suite.chainA.App.AppCodec(), payload.ClientState).(*ibctm.ClientState) suite.Require().True(ok) clientState := types.NewClientState(payload.ClientState, payload.Checksum, wrappedClientState.LatestHeight) clientStateBz := clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), clientState) store.Set(host.ClientStateKey(), clientStateBz) consensusState := types.NewConsensusState(payload.ConsensusState) consensusStateBz := clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), consensusState) store.Set(host.ConsensusStateKey(clientState.LatestHeight), consensusStateBz) resp, err := json.Marshal(types.EmptyResult{}) suite.Require().NoError(err) return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: resp}}, 0, nil } suite.mockVM.RegisterQueryCallback(types.StatusMsg{}, func(checksum wasmvm.Checksum, env wasmvmtypes.Env, queryMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { resp, err := json.Marshal(types.StatusResult{Status: exported.Active.String()}) suite.Require().NoError(err) return &wasmvmtypes.QueryResult{Ok: resp}, wasmtesting.DefaultGasUsed, nil }) db := dbm.NewMemDB() app := simapp.NewUnitTestSimApp(log.NewNopLogger(), db, nil, true, simtestutil.EmptyAppOptions{}, suite.mockVM) return app, app.DefaultGenesis() } // storeWasmCode stores the wasm code on chain and returns the checksum. func (suite *KeeperTestSuite) storeWasmCode(wasmCode []byte) []byte { ctx := suite.chainA.GetContext().WithBlockGasMeter(storetypes.NewInfiniteGasMeter()) msg := types.NewMsgStoreCode(authtypes.NewModuleAddress(govtypes.ModuleName).String(), wasmCode) response, err := GetSimApp(suite.chainA).WasmClientKeeper.StoreCode(ctx, msg) suite.Require().NoError(err) suite.Require().NotNil(response.Checksum) return response.Checksum } func (suite *KeeperTestSuite) SetupSnapshotterWithMockVM() *simapp.SimApp { suite.mockVM = wasmtesting.NewMockWasmEngine() return simapp.SetupWithSnapshotter(suite.T(), suite.mockVM) } func TestKeeperTestSuite(t *testing.T) { testifysuite.Run(t, new(KeeperTestSuite)) } func (suite *KeeperTestSuite) TestNewKeeper() { testCases := []struct { name string instantiateFn func() expError error }{ { "success", func() { keeper.NewKeeperWithVM( GetSimApp(suite.chainA).AppCodec(), runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), GetSimApp(suite.chainA).WasmClientKeeper.GetVM(), GetSimApp(suite.chainA).GRPCQueryRouter(), ) }, nil, }, { "failure: empty authority", func() { keeper.NewKeeperWithVM( GetSimApp(suite.chainA).AppCodec(), runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, "", // authority GetSimApp(suite.chainA).WasmClientKeeper.GetVM(), GetSimApp(suite.chainA).GRPCQueryRouter(), ) }, errors.New("authority must be non-empty"), }, { "failure: nil client keeper", func() { keeper.NewKeeperWithVM( GetSimApp(suite.chainA).AppCodec(), runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), nil, // client keeper, GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), GetSimApp(suite.chainA).WasmClientKeeper.GetVM(), GetSimApp(suite.chainA).GRPCQueryRouter(), ) }, errors.New("client keeper must not be nil"), }, { "failure: nil wasm VM", func() { keeper.NewKeeperWithVM( GetSimApp(suite.chainA).AppCodec(), runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), nil, GetSimApp(suite.chainA).GRPCQueryRouter(), ) }, errors.New("wasm VM must not be nil"), }, { "failure: nil store service", func() { keeper.NewKeeperWithVM( GetSimApp(suite.chainA).AppCodec(), nil, GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), GetSimApp(suite.chainA).WasmClientKeeper.GetVM(), GetSimApp(suite.chainA).GRPCQueryRouter(), ) }, errors.New("store service must not be nil"), }, { "failure: nil query router", func() { keeper.NewKeeperWithVM( GetSimApp(suite.chainA).AppCodec(), runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), GetSimApp(suite.chainA).WasmClientKeeper.GetVM(), nil, ) }, errors.New("query router must not be nil"), }, } for _, tc := range testCases { tc := tc suite.SetupTest() suite.Run(tc.name, func() { if tc.expError == nil { suite.Require().NotPanics( tc.instantiateFn, ) } else { suite.Require().PanicsWithError(tc.expError.Error(), func() { tc.instantiateFn() }) } }) } } func (suite *KeeperTestSuite) TestInitializedPinnedCodes() { var capturedChecksums []wasmvm.Checksum testCases := []struct { name string malleate func() expError error }{ { "success", func() { suite.mockVM.PinFn = func(checksum wasmvm.Checksum) error { capturedChecksums = append(capturedChecksums, checksum) return nil } }, nil, }, { "failure: pin error", func() { suite.mockVM.PinFn = func(checksum wasmvm.Checksum) error { return wasmtesting.ErrMockVM } }, wasmtesting.ErrMockVM, }, } for _, tc := range testCases { tc := tc suite.Run(tc.name, func() { suite.SetupWasmWithMockVM() ctx := suite.chainA.GetContext() wasmClientKeeper := GetSimApp(suite.chainA).WasmClientKeeper contracts := [][]byte{wasmtesting.Code, wasmtesting.CreateMockContract([]byte("gzipped-contract"))} checksumIDs := make([]types.Checksum, len(contracts)) signer := authtypes.NewModuleAddress(govtypes.ModuleName).String() // store contract on chain for i, contract := range contracts { msg := types.NewMsgStoreCode(signer, contract) res, err := wasmClientKeeper.StoreCode(ctx, msg) suite.Require().NoError(err) checksumIDs[i] = res.Checksum } // malleate after storing contracts tc.malleate() err := wasmClientKeeper.InitializePinnedCodes(ctx) if tc.expError == nil { suite.Require().NoError(err) suite.ElementsMatch(checksumIDs, capturedChecksums) } else { suite.Require().ErrorIs(err, tc.expError) } }) } } func (suite *KeeperTestSuite) TestMigrateContract() { var ( oldHash []byte newHash []byte payload []byte expClientState *types.ClientState ) testCases := []struct { name string malleate func() expErr error }{ { "success: update client state", func() { suite.mockVM.MigrateFn = func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { expClientState = types.NewClientState([]byte{1}, newHash, clienttypes.NewHeight(2000, 2)) store.Set(host.ClientStateKey(), clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), expClientState)) data, err := json.Marshal(types.EmptyResult{}) suite.Require().NoError(err) return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: data}}, wasmtesting.DefaultGasUsed, nil } }, nil, }, { "failure: new and old checksum are the same", func() { newHash = oldHash // this should not be called suite.mockVM.MigrateFn = func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { panic("unreachable") } }, types.ErrWasmCodeExists, }, { "failure: checksum not found", func() { err := GetSimApp(suite.chainA).WasmClientKeeper.GetChecksums().Remove(suite.chainA.GetContext(), newHash) suite.Require().NoError(err) }, types.ErrWasmChecksumNotFound, }, { "failure: vm returns error", func() { err := GetSimApp(suite.chainA).WasmClientKeeper.GetChecksums().Set(suite.chainA.GetContext(), newHash) suite.Require().NoError(err) suite.mockVM.MigrateFn = func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { return nil, wasmtesting.DefaultGasUsed, wasmtesting.ErrMockVM } }, types.ErrVMError, }, { "failure: contract returns error", func() { err := GetSimApp(suite.chainA).WasmClientKeeper.GetChecksums().Set(suite.chainA.GetContext(), newHash) suite.Require().NoError(err) suite.mockVM.MigrateFn = func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { return &wasmvmtypes.ContractResult{Err: wasmtesting.ErrMockContract.Error()}, wasmtesting.DefaultGasUsed, nil } }, types.ErrWasmContractCallFailed, }, } for _, tc := range testCases { tc := tc suite.Run(tc.name, func() { suite.SetupWasmWithMockVM() suite.storeWasmCode(wasmtesting.Code) var err error oldHash, err = types.CreateChecksum(wasmtesting.Code) suite.Require().NoError(err) newHash, err = types.CreateChecksum(wasmtesting.CreateMockContract([]byte{1, 2, 3})) suite.Require().NoError(err) wasmClientKeeper := GetSimApp(suite.chainA).WasmClientKeeper err = wasmClientKeeper.GetChecksums().Set(suite.chainA.GetContext(), newHash) suite.Require().NoError(err) endpointA := wasmtesting.NewWasmEndpoint(suite.chainA) err = endpointA.CreateClient() suite.Require().NoError(err) clientState, ok := endpointA.GetClientState().(*types.ClientState) suite.Require().True(ok) expClientState = clientState tc.malleate() err = wasmClientKeeper.MigrateContractCode(suite.chainA.GetContext(), endpointA.ClientID, newHash, payload) // updated client state clientState, ok = endpointA.GetClientState().(*types.ClientState) suite.Require().True(ok) if tc.expErr == nil { suite.Require().NoError(err) suite.Require().Equal(expClientState, clientState) } else { suite.Require().ErrorIs(err, tc.expErr) } }) } } func (suite *KeeperTestSuite) TestGetChecksums() { testCases := []struct { name string malleate func() expResult func(checksums []types.Checksum) }{ { "success: no contract stored.", func() {}, func(checksums []types.Checksum) { suite.Require().Len(checksums, 0) }, }, { "success: default mock vm contract stored.", func() { suite.SetupWasmWithMockVM() suite.storeWasmCode(wasmtesting.Code) }, func(checksums []types.Checksum) { suite.Require().Len(checksums, 1) expectedChecksum, err := types.CreateChecksum(wasmtesting.Code) suite.Require().NoError(err) suite.Require().Equal(expectedChecksum, checksums[0]) }, }, { "success: non-empty checksums", func() { suite.SetupWasmWithMockVM() suite.storeWasmCode(wasmtesting.Code) err := GetSimApp(suite.chainA).WasmClientKeeper.GetChecksums().Set(suite.chainA.GetContext(), types.Checksum("checksum")) suite.Require().NoError(err) }, func(checksums []types.Checksum) { suite.Require().Len(checksums, 2) suite.Require().Contains(checksums, types.Checksum("checksum")) }, }, } for _, tc := range testCases { tc := tc suite.Run(tc.name, func() { tc.malleate() checksums, err := GetSimApp(suite.chainA).WasmClientKeeper.GetAllChecksums(suite.chainA.GetContext()) suite.Require().NoError(err) tc.expResult(checksums) }) } } func (suite *KeeperTestSuite) TestAddChecksum() { suite.SetupWasmWithMockVM() suite.storeWasmCode(wasmtesting.Code) wasmClientKeeper := GetSimApp(suite.chainA).WasmClientKeeper checksums, err := wasmClientKeeper.GetAllChecksums(suite.chainA.GetContext()) suite.Require().NoError(err) // default mock vm contract is stored suite.Require().Len(checksums, 1) checksum1 := types.Checksum("checksum1") checksum2 := types.Checksum("checksum2") err = wasmClientKeeper.GetChecksums().Set(suite.chainA.GetContext(), checksum1) suite.Require().NoError(err) err = wasmClientKeeper.GetChecksums().Set(suite.chainA.GetContext(), checksum2) suite.Require().NoError(err) // Test adding the same checksum twice err = wasmClientKeeper.GetChecksums().Set(suite.chainA.GetContext(), checksum1) suite.Require().NoError(err) checksums, err = wasmClientKeeper.GetAllChecksums(suite.chainA.GetContext()) suite.Require().NoError(err) suite.Require().Len(checksums, 3) suite.Require().Contains(checksums, checksum1) suite.Require().Contains(checksums, checksum2) } func (suite *KeeperTestSuite) TestHasChecksum() { var checksum types.Checksum testCases := []struct { name string malleate func() exprResult bool }{ { "success: checksum exists", func() { checksum = types.Checksum("checksum") err := GetSimApp(suite.chainA).WasmClientKeeper.GetChecksums().Set(suite.chainA.GetContext(), checksum) suite.Require().NoError(err) }, true, }, { "success: checksum does not exist", func() { checksum = types.Checksum("non-existent-checksum") }, false, }, } for _, tc := range testCases { tc := tc suite.Run(tc.name, func() { suite.SetupWasmWithMockVM() tc.malleate() result := GetSimApp(suite.chainA).WasmClientKeeper.HasChecksum(suite.chainA.GetContext(), checksum) suite.Require().Equal(tc.exprResult, result) }) } }