From 768d592f35e42cd26bb660c5f79c9e4eef0e12d6 Mon Sep 17 00:00:00 2001 From: Carlos Rodriguez Date: Mon, 20 Nov 2023 20:07:11 +0100 Subject: [PATCH 1/2] imp: use wasm VM CreateChecksum (#5123) * use wasm VM CreateChecksum * review comments * lint * lint 2 * Remove raw calls to sha256, clean up handling of Checksums. * Update modules/light-clients/08-wasm/keeper/keeper.go Co-authored-by: Damian Nolan * Document this bad boy. --------- Co-authored-by: DimitrisJim Co-authored-by: Damian Nolan (cherry picked from commit a231feab61c8fa9ada8abf602bf15cb3625230e3) # Conflicts: # modules/light-clients/08-wasm/keeper/events.go # modules/light-clients/08-wasm/keeper/genesis.go # modules/light-clients/08-wasm/keeper/keeper.go # modules/light-clients/08-wasm/keeper/msg_server_test.go # modules/light-clients/08-wasm/keeper/snapshotter.go # modules/light-clients/08-wasm/keeper/snapshotter_test.go # modules/light-clients/08-wasm/testing/mock_engine.go # modules/light-clients/08-wasm/testing/values.go # modules/light-clients/08-wasm/testing/wasm_endpoint.go # modules/light-clients/08-wasm/types/client_state_test.go # modules/light-clients/08-wasm/types/migrate_contract_test.go # modules/light-clients/08-wasm/types/msgs_test.go # modules/light-clients/08-wasm/types/types_test.go # modules/light-clients/08-wasm/types/validation.go # modules/light-clients/08-wasm/types/validation_test.go # modules/light-clients/08-wasm/types/vm.go # modules/light-clients/08-wasm/types/wasm.go # modules/light-clients/08-wasm/types/wasm_test.go --- .../light-clients/08-wasm/keeper/events.go | 39 ++ .../light-clients/08-wasm/keeper/genesis.go | 42 ++ .../light-clients/08-wasm/keeper/keeper.go | 192 ++++++ .../08-wasm/keeper/msg_server_test.go | 416 ++++++++++++ .../08-wasm/keeper/snapshotter.go | 146 +++++ .../08-wasm/keeper/snapshotter_test.go | 121 ++++ .../08-wasm/testing/mock_engine.go | 268 ++++++++ .../light-clients/08-wasm/testing/values.go | 33 + .../08-wasm/testing/wasm_endpoint.go | 49 ++ .../08-wasm/types/client_state_test.go | 619 ++++++++++++++++++ .../08-wasm/types/migrate_contract_test.go | 141 ++++ .../light-clients/08-wasm/types/msgs_test.go | 268 ++++++++ .../light-clients/08-wasm/types/types_test.go | 124 ++++ .../light-clients/08-wasm/types/validation.go | 58 ++ .../08-wasm/types/validation_test.go | 162 +++++ modules/light-clients/08-wasm/types/vm.go | 313 +++++++++ modules/light-clients/08-wasm/types/wasm.go | 54 ++ .../light-clients/08-wasm/types/wasm_test.go | 124 ++++ 18 files changed, 3169 insertions(+) create mode 100644 modules/light-clients/08-wasm/keeper/events.go create mode 100644 modules/light-clients/08-wasm/keeper/genesis.go create mode 100644 modules/light-clients/08-wasm/keeper/keeper.go create mode 100644 modules/light-clients/08-wasm/keeper/msg_server_test.go create mode 100644 modules/light-clients/08-wasm/keeper/snapshotter.go create mode 100644 modules/light-clients/08-wasm/keeper/snapshotter_test.go create mode 100644 modules/light-clients/08-wasm/testing/mock_engine.go create mode 100644 modules/light-clients/08-wasm/testing/values.go create mode 100644 modules/light-clients/08-wasm/testing/wasm_endpoint.go create mode 100644 modules/light-clients/08-wasm/types/client_state_test.go create mode 100644 modules/light-clients/08-wasm/types/migrate_contract_test.go create mode 100644 modules/light-clients/08-wasm/types/msgs_test.go create mode 100644 modules/light-clients/08-wasm/types/types_test.go create mode 100644 modules/light-clients/08-wasm/types/validation.go create mode 100644 modules/light-clients/08-wasm/types/validation_test.go create mode 100644 modules/light-clients/08-wasm/types/vm.go create mode 100644 modules/light-clients/08-wasm/types/wasm.go create mode 100644 modules/light-clients/08-wasm/types/wasm_test.go diff --git a/modules/light-clients/08-wasm/keeper/events.go b/modules/light-clients/08-wasm/keeper/events.go new file mode 100644 index 00000000000..8e9801b9e45 --- /dev/null +++ b/modules/light-clients/08-wasm/keeper/events.go @@ -0,0 +1,39 @@ +package keeper + +import ( + "encoding/hex" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" +) + +// emitStoreWasmCodeEvent emits a store wasm code event +func emitStoreWasmCodeEvent(ctx sdk.Context, checksum types.Checksum) { + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeStoreWasmCode, + sdk.NewAttribute(types.AttributeKeyWasmChecksum, hex.EncodeToString(checksum)), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + ), + }) +} + +// emitMigrateContractEvent emits a migrate contract event +func emitMigrateContractEvent(ctx sdk.Context, clientID string, checksum, newChecksum types.Checksum) { + ctx.EventManager().EmitEvents(sdk.Events{ + sdk.NewEvent( + types.EventTypeMigrateContract, + sdk.NewAttribute(types.AttributeKeyClientID, clientID), + sdk.NewAttribute(types.AttributeKeyWasmChecksum, hex.EncodeToString(checksum)), + sdk.NewAttribute(types.AttributeKeyNewChecksum, hex.EncodeToString(newChecksum)), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + ), + }) +} diff --git a/modules/light-clients/08-wasm/keeper/genesis.go b/modules/light-clients/08-wasm/keeper/genesis.go new file mode 100644 index 00000000000..4384fb1f599 --- /dev/null +++ b/modules/light-clients/08-wasm/keeper/genesis.go @@ -0,0 +1,42 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" +) + +// InitGenesis initializes the 08-wasm module's state from a provided genesis +// state. +func (k Keeper) InitGenesis(ctx sdk.Context, gs types.GenesisState) error { + for _, contract := range gs.Contracts { + _, err := k.storeWasmCode(ctx, contract.CodeBytes) + if err != nil { + return err + } + } + return nil +} + +// ExportGenesis returns the 08-wasm module's exported genesis. This includes the code +// for all contracts previously stored. +func (k Keeper) ExportGenesis(ctx sdk.Context) types.GenesisState { + checksums, err := types.GetAllChecksums(ctx) + if err != nil { + panic(err) + } + + // Grab code from wasmVM and add to genesis state. + var genesisState types.GenesisState + for _, checksum := range checksums { + code, err := k.wasmVM.GetCode(checksum) + if err != nil { + panic(err) + } + genesisState.Contracts = append(genesisState.Contracts, types.Contract{ + CodeBytes: code, + }) + } + + return genesisState +} diff --git a/modules/light-clients/08-wasm/keeper/keeper.go b/modules/light-clients/08-wasm/keeper/keeper.go new file mode 100644 index 00000000000..4b65bba05b1 --- /dev/null +++ b/modules/light-clients/08-wasm/keeper/keeper.go @@ -0,0 +1,192 @@ +package keeper + +import ( + "bytes" + "encoding/hex" + "errors" + "fmt" + "strings" + + wasmvm "github.com/CosmWasm/wasmvm" + + storetypes "cosmossdk.io/core/store" + errorsmod "cosmossdk.io/errors" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" +) + +// Keeper defines the 08-wasm keeper +type Keeper struct { + // implements gRPC QueryServer interface + types.QueryServer + + cdc codec.BinaryCodec + wasmVM ibcwasm.WasmEngine + + clientKeeper types.ClientKeeper + + authority string +} + +// NewKeeperWithVM creates a new Keeper instance with the provided Wasm VM. +// This constructor function is meant to be used when the chain uses x/wasm +// and the same Wasm VM instance should be shared with it. +func NewKeeperWithVM( + cdc codec.BinaryCodec, + storeService storetypes.KVStoreService, + clientKeeper types.ClientKeeper, + authority string, + vm ibcwasm.WasmEngine, +) Keeper { + if clientKeeper == nil { + panic(errors.New("client keeper must be not nil")) + } + + if vm == nil { + panic(errors.New("wasm VM must be not nil")) + } + + if storeService == nil { + panic(errors.New("store service must be not nil")) + } + + if strings.TrimSpace(authority) == "" { + panic(errors.New("authority must be non-empty")) + } + + ibcwasm.SetVM(vm) + ibcwasm.SetupWasmStoreService(storeService) + + return Keeper{ + cdc: cdc, + wasmVM: vm, + clientKeeper: clientKeeper, + authority: authority, + } +} + +// NewKeeperWithConfig creates a new Keeper instance with the provided Wasm configuration. +// This constructor function is meant to be used when the chain does not use x/wasm +// and a Wasm VM needs to be instantiated using the provided parameters. +func NewKeeperWithConfig( + cdc codec.BinaryCodec, + storeService storetypes.KVStoreService, + clientKeeper types.ClientKeeper, + authority string, + wasmConfig types.WasmConfig, +) Keeper { + vm, err := wasmvm.NewVM(wasmConfig.DataDir, wasmConfig.SupportedCapabilities, types.ContractMemoryLimit, wasmConfig.ContractDebugMode, types.MemoryCacheSize) + if err != nil { + panic(fmt.Errorf("failed to instantiate new Wasm VM instance: %v", err)) + } + + return NewKeeperWithVM(cdc, storeService, clientKeeper, authority, vm) +} + +// GetAuthority returns the 08-wasm module's authority. +func (k Keeper) GetAuthority() string { + return k.authority +} + +func (k Keeper) storeWasmCode(ctx sdk.Context, code []byte) ([]byte, error) { + var err error + if types.IsGzip(code) { + ctx.GasMeter().ConsumeGas(types.VMGasRegister.UncompressCosts(len(code)), "Uncompress gzip bytecode") + code, err = types.Uncompress(code, types.MaxWasmByteSize()) + if err != nil { + return nil, errorsmod.Wrap(err, "failed to store contract") + } + } + + // Check to see if store already has checksum. + checksum, err := types.CreateChecksum(code) + if err != nil { + return nil, errorsmod.Wrap(err, "wasm bytecode checksum failed") + } + + if types.HasChecksum(ctx, checksum) { + return nil, types.ErrWasmCodeExists + } + + // run the code through the wasm light client validation process + if err := types.ValidateWasmCode(code); err != nil { + return nil, errorsmod.Wrap(err, "wasm bytecode validation failed") + } + + // create the code in the vm + ctx.GasMeter().ConsumeGas(types.VMGasRegister.CompileCosts(len(code)), "Compiling wasm bytecode") + vmChecksum, err := k.wasmVM.StoreCode(code) + if err != nil { + return nil, errorsmod.Wrap(err, "failed to store contract") + } + + // SANITY: We've checked our store, additional safety check to assert that the checksum returned by WasmVM equals checksum generated by us. + if !bytes.Equal(vmChecksum, checksum) { + return nil, errorsmod.Wrapf(types.ErrInvalidChecksum, "expected %s, got %s", hex.EncodeToString(checksum), hex.EncodeToString(vmChecksum)) + } + + // pin the code to the vm in-memory cache + if err := k.wasmVM.Pin(vmChecksum); err != nil { + return nil, errorsmod.Wrapf(err, "failed to pin contract with checksum (%s) to vm cache", hex.EncodeToString(vmChecksum)) + } + + // store the checksum + err = ibcwasm.Checksums.Set(ctx, checksum) + if err != nil { + return nil, errorsmod.Wrap(err, "failed to store checksum") + } + + return checksum, nil +} + +func (k Keeper) migrateContractCode(ctx sdk.Context, clientID string, newChecksum, migrateMsg []byte) error { + wasmClientState, err := k.GetWasmClientState(ctx, clientID) + if err != nil { + return errorsmod.Wrap(err, "failed to retrieve wasm client state") + } + oldChecksum := wasmClientState.Checksum + + clientStore := k.clientKeeper.ClientStore(ctx, clientID) + + err = wasmClientState.MigrateContract(ctx, k.cdc, clientStore, clientID, newChecksum, migrateMsg) + if err != nil { + return errorsmod.Wrap(err, "contract migration failed") + } + + // client state may be updated by the contract migration + wasmClientState, err = k.GetWasmClientState(ctx, clientID) + if err != nil { + // note that this also ensures that the updated client state is + // still a wasm client state + return errorsmod.Wrap(err, "failed to retrieve the updated wasm client state") + } + + // update the client state checksum before persisting it + wasmClientState.Checksum = newChecksum + + k.clientKeeper.SetClientState(ctx, clientID, wasmClientState) + + emitMigrateContractEvent(ctx, clientID, oldChecksum, newChecksum) + + return nil +} + +// GetWasmClientState returns the 08-wasm client state for the given client identifier. +func (k Keeper) GetWasmClientState(ctx sdk.Context, clientID string) (*types.ClientState, error) { + clientState, found := k.clientKeeper.GetClientState(ctx, clientID) + if !found { + return nil, errorsmod.Wrapf(clienttypes.ErrClientTypeNotFound, "clientID %s", clientID) + } + + wasmClientState, ok := clientState.(*types.ClientState) + if !ok { + return nil, errorsmod.Wrapf(clienttypes.ErrInvalidClient, "expected type %T, got %T", (*types.ClientState)(nil), wasmClientState) + } + + return wasmClientState, nil +} diff --git a/modules/light-clients/08-wasm/keeper/msg_server_test.go b/modules/light-clients/08-wasm/keeper/msg_server_test.go new file mode 100644 index 00000000000..080e1350e63 --- /dev/null +++ b/modules/light-clients/08-wasm/keeper/msg_server_test.go @@ -0,0 +1,416 @@ +package keeper_test + +import ( + "encoding/hex" + "encoding/json" + "errors" + "os" + + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" + wasmtesting "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + host "github.com/cosmos/ibc-go/v8/modules/core/24-host" + ibcerrors "github.com/cosmos/ibc-go/v8/modules/core/errors" + localhost "github.com/cosmos/ibc-go/v8/modules/light-clients/09-localhost" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +func (suite *KeeperTestSuite) TestMsgStoreCode() { + var ( + msg *types.MsgStoreCode + signer string + data []byte + ) + + testCases := []struct { + name string + malleate func() + expError error + }{ + { + "success", + func() { + msg = types.NewMsgStoreCode(signer, data) + }, + nil, + }, + { + "fails with duplicate wasm code", + func() { + msg = types.NewMsgStoreCode(signer, data) + + _, err := GetSimApp(suite.chainA).WasmClientKeeper.StoreCode(suite.chainA.GetContext(), msg) + suite.Require().NoError(err) + }, + types.ErrWasmCodeExists, + }, + { + "fails with zero-length wasm code", + func() { + msg = types.NewMsgStoreCode(signer, []byte{}) + }, + errors.New("Wasm bytes nil or empty"), + }, + { + "fails with checksum", + func() { + msg = types.NewMsgStoreCode(signer, []byte{0, 1, 3, 4}) + }, + errors.New("Wasm bytes do not not start with Wasm magic number"), + }, + { + "fails with wasm code too large", + func() { + msg = types.NewMsgStoreCode(signer, append(wasmtesting.WasmMagicNumber, []byte(ibctesting.GenerateString(uint(types.MaxWasmByteSize())))...)) + }, + types.ErrWasmCodeTooLarge, + }, + { + "fails with unauthorized signer", + func() { + signer = suite.chainA.SenderAccount.GetAddress().String() + msg = types.NewMsgStoreCode(signer, data) + }, + ibcerrors.ErrUnauthorized, + }, + { + "failure: checksum could not be pinned", + func() { + msg = types.NewMsgStoreCode(signer, data) + + suite.mockVM.PinFn = func(_ wasmvm.Checksum) error { + return wasmtesting.ErrMockVM + } + }, + wasmtesting.ErrMockVM, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() + + signer = authtypes.NewModuleAddress(govtypes.ModuleName).String() + data, _ = os.ReadFile("../test_data/ics10_grandpa_cw.wasm.gz") + + tc.malleate() + + ctx := suite.chainA.GetContext() + res, err := GetSimApp(suite.chainA).WasmClientKeeper.StoreCode(ctx, msg) + events := ctx.EventManager().Events() + + if tc.expError == nil { + suite.Require().NoError(err) + suite.Require().NotNil(res) + suite.Require().NotEmpty(res.Checksum) + + // Verify events + expectedEvents := sdk.Events{ + sdk.NewEvent( + "store_wasm_code", + sdk.NewAttribute(types.AttributeKeyWasmChecksum, hex.EncodeToString(res.Checksum)), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + ), + } + + for _, evt := range expectedEvents { + suite.Require().Contains(events, evt) + } + } else { + suite.Require().Contains(err.Error(), tc.expError.Error()) + suite.Require().Nil(res) + suite.Require().Empty(events) + } + }) + } +} + +func (suite *KeeperTestSuite) TestMsgMigrateContract() { + oldChecksum, err := types.CreateChecksum(wasmtesting.Code) + suite.Require().NoError(err) + + newByteCode := wasmtesting.WasmMagicNumber + newByteCode = append(newByteCode, []byte("MockByteCode-TestMsgMigrateContract")...) + + govAcc := authtypes.NewModuleAddress(govtypes.ModuleName).String() + + var ( + newChecksum []byte + msg *types.MsgMigrateContract + expClientState *types.ClientState + ) + + testCases := []struct { + name string + malleate func() + expError error + }{ + { + "success: no update to client state", + func() { + msg = types.NewMsgMigrateContract(govAcc, defaultWasmClientID, newChecksum, []byte("{}")) + + suite.mockVM.MigrateFn = func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + data, err := json.Marshal(types.EmptyResult{}) + suite.Require().NoError(err) + + return &wasmvmtypes.Response{Data: data}, wasmtesting.DefaultGasUsed, nil + } + }, + nil, + }, + { + "success: update client state", + func() { + msg = types.NewMsgMigrateContract(govAcc, defaultWasmClientID, newChecksum, []byte("{}")) + + suite.mockVM.MigrateFn = func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + // the checksum written in the client state will later be overwritten by the message server. + expClientState = types.NewClientState([]byte{1}, []byte("invalid checksum"), 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.Response{Data: data}, wasmtesting.DefaultGasUsed, nil + } + }, + nil, + }, + { + "failure: same checksum", + func() { + msg = types.NewMsgMigrateContract(govAcc, defaultWasmClientID, oldChecksum, []byte("{}")) + + suite.mockVM.MigrateFn = func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + panic("unreachable") + } + }, + types.ErrWasmCodeExists, + }, + { + "failure: unauthorized signer", + func() { + msg = types.NewMsgMigrateContract(suite.chainA.SenderAccount.GetAddress().String(), defaultWasmClientID, newChecksum, []byte("{}")) + }, + ibcerrors.ErrUnauthorized, + }, + { + "failure: invalid wasm checksum", + func() { + msg = types.NewMsgMigrateContract(govAcc, defaultWasmClientID, []byte(ibctesting.InvalidID), []byte("{}")) + }, + types.ErrWasmChecksumNotFound, + }, + { + "failure: invalid client id", + func() { + msg = types.NewMsgMigrateContract(govAcc, ibctesting.InvalidID, newChecksum, []byte("{}")) + }, + clienttypes.ErrClientTypeNotFound, + }, + { + "failure: contract returns error", + func() { + msg = types.NewMsgMigrateContract(govAcc, defaultWasmClientID, newChecksum, []byte("{}")) + + suite.mockVM.MigrateFn = func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + return nil, wasmtesting.DefaultGasUsed, wasmtesting.ErrMockContract + } + }, + types.ErrWasmContractCallFailed, + }, + { + "failure: incorrect state update", + func() { + msg = types.NewMsgMigrateContract(govAcc, defaultWasmClientID, newChecksum, []byte("{}")) + + suite.mockVM.MigrateFn = func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + // the checksum written in here will be overwritten + newClientState := localhost.NewClientState(clienttypes.NewHeight(1, 1)) + + store.Set(host.ClientStateKey(), clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), newClientState)) + + data, err := json.Marshal(types.EmptyResult{}) + suite.Require().NoError(err) + + return &wasmvmtypes.Response{Data: data}, wasmtesting.DefaultGasUsed, nil + } + }, + types.ErrWasmInvalidContractModification, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() + + newChecksum = storeWasmCode(suite, newByteCode) + + endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) + err := endpoint.CreateClient() + suite.Require().NoError(err) + + // this is the old client state + expClientState = endpoint.GetClientState().(*types.ClientState) + + tc.malleate() + + ctx := suite.chainA.GetContext() + res, err := GetSimApp(suite.chainA).WasmClientKeeper.MigrateContract(ctx, msg) + events := ctx.EventManager().Events().ToABCIEvents() + + if tc.expError == nil { + expClientState.Checksum = newChecksum + + suite.Require().NoError(err) + suite.Require().NotNil(res) + + // updated client state + clientState, ok := endpoint.GetClientState().(*types.ClientState) + suite.Require().True(ok) + + suite.Require().Equal(expClientState, clientState) + + // Verify events + expectedEvents := sdk.Events{ + sdk.NewEvent( + "migrate_contract", + sdk.NewAttribute(types.AttributeKeyClientID, defaultWasmClientID), + sdk.NewAttribute(types.AttributeKeyWasmChecksum, hex.EncodeToString(oldChecksum)), + sdk.NewAttribute(types.AttributeKeyNewChecksum, hex.EncodeToString(newChecksum)), + ), + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + ), + }.ToABCIEvents() + + for _, evt := range expectedEvents { + suite.Require().Contains(events, evt) + } + } else { + suite.Require().ErrorIs(err, tc.expError) + suite.Require().Nil(res) + } + }) + } +} + +func (suite *KeeperTestSuite) TestMsgRemoveChecksum() { + checksum, err := types.CreateChecksum(wasmtesting.Code) + suite.Require().NoError(err) + + govAcc := authtypes.NewModuleAddress(govtypes.ModuleName).String() + + var ( + msg *types.MsgRemoveChecksum + expChecksums []types.Checksum + ) + + testCases := []struct { + name string + malleate func() + expError error + }{ + { + "success", + func() { + msg = types.NewMsgRemoveChecksum(govAcc, checksum) + + expChecksums = []types.Checksum{} + }, + nil, + }, + { + "success: many checksums", + func() { + msg = types.NewMsgRemoveChecksum(govAcc, checksum) + + expChecksums = []types.Checksum{} + + for i := 0; i < 20; i++ { + mockCode := []byte{byte(i)} + mockCode = append(wasmtesting.WasmMagicNumber, mockCode...) + checksum, err := types.CreateChecksum(mockCode) + suite.Require().NoError(err) + + err = ibcwasm.Checksums.Set(suite.chainA.GetContext(), checksum) + suite.Require().NoError(err) + + expChecksums = append(expChecksums, checksum) + } + }, + nil, + }, + { + "failure: checksum is missing", + func() { + msg = types.NewMsgRemoveChecksum(govAcc, []byte{1}) + }, + types.ErrWasmChecksumNotFound, + }, + { + "failure: unauthorized signer", + func() { + msg = types.NewMsgRemoveChecksum(suite.chainA.SenderAccount.GetAddress().String(), checksum) + }, + ibcerrors.ErrUnauthorized, + }, + { + "failure: code has could not be unpinned", + func() { + msg = types.NewMsgRemoveChecksum(govAcc, checksum) + + suite.mockVM.UnpinFn = func(_ wasmvm.Checksum) error { + return wasmtesting.ErrMockVM + } + }, + wasmtesting.ErrMockVM, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() + + endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) + err := endpoint.CreateClient() + suite.Require().NoError(err) + + tc.malleate() + + ctx := suite.chainA.GetContext() + res, err := GetSimApp(suite.chainA).WasmClientKeeper.RemoveChecksum(ctx, msg) + events := ctx.EventManager().Events().ToABCIEvents() + + if tc.expError == nil { + suite.Require().NoError(err) + suite.Require().NotNil(res) + + checksums, err := types.GetAllChecksums(suite.chainA.GetContext()) + suite.Require().NoError(err) + + // Check equality of checksums up to order + suite.Require().ElementsMatch(expChecksums, checksums) + + // Verify events + suite.Require().Len(events, 0) + } else { + suite.Require().ErrorIs(err, tc.expError) + suite.Require().Nil(res) + } + }) + } +} diff --git a/modules/light-clients/08-wasm/keeper/snapshotter.go b/modules/light-clients/08-wasm/keeper/snapshotter.go new file mode 100644 index 00000000000..e48baff5678 --- /dev/null +++ b/modules/light-clients/08-wasm/keeper/snapshotter.go @@ -0,0 +1,146 @@ +package keeper + +import ( + "encoding/hex" + "io" + + errorsmod "cosmossdk.io/errors" + snapshot "cosmossdk.io/store/snapshots/types" + storetypes "cosmossdk.io/store/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" +) + +var _ snapshot.ExtensionSnapshotter = &WasmSnapshotter{} + +// SnapshotFormat defines the default snapshot extension encoding format. +// SnapshotFormat 1 is gzipped wasm byte code for each item payload. No protobuf envelope, no metadata. +const SnapshotFormat = 1 + +// WasmSnapshotter implements the snapshot.ExtensionSnapshotter interface and is used to +// import and export state maintained within the wasmvm cache. +// NOTE: The following ExtensionSnapshotter has been adapted from CosmWasm's x/wasm: +// https://github.com/CosmWasm/wasmd/blob/v0.43.0/x/wasm/keeper/snapshotter.go +type WasmSnapshotter struct { + cms storetypes.MultiStore + keeper *Keeper +} + +// NewWasmSnapshotter creates and returns a new snapshot.ExtensionSnapshotter implementation for the 08-wasm module. +func NewWasmSnapshotter(cms storetypes.MultiStore, keeper *Keeper) snapshot.ExtensionSnapshotter { + return &WasmSnapshotter{ + cms: cms, + keeper: keeper, + } +} + +// SnapshotName implements the snapshot.ExtensionSnapshotter interface. +// A unique name should be provided such that the implementation can be identified by the manager. +func (*WasmSnapshotter) SnapshotName() string { + return types.ModuleName +} + +// SnapshotFormat implements the snapshot.ExtensionSnapshotter interface. +// This is the default format used for encoding payloads when taking a snapshot. +func (*WasmSnapshotter) SnapshotFormat() uint32 { + return SnapshotFormat +} + +// SupportedFormats implements the snapshot.ExtensionSnapshotter interface. +// This defines a list of supported formats the snapshotter extension can restore from. +func (*WasmSnapshotter) SupportedFormats() []uint32 { + return []uint32{SnapshotFormat} +} + +// SnapshotExtension implements the snapshot.ExntensionSnapshotter interface. +// SnapshotExtension is used to write data payloads into the underlying protobuf stream from the 08-wasm module. +func (ws *WasmSnapshotter) SnapshotExtension(height uint64, payloadWriter snapshot.ExtensionPayloadWriter) error { + cacheMS, err := ws.cms.CacheMultiStoreWithVersion(int64(height)) + if err != nil { + return err + } + + ctx := sdk.NewContext(cacheMS, tmproto.Header{}, false, nil) + + checksums, err := types.GetAllChecksums(ctx) + if err != nil { + return err + } + + for _, checksum := range checksums { + wasmCode, err := ws.keeper.wasmVM.GetCode(checksum) + if err != nil { + return err + } + + compressedWasm, err := types.GzipIt(wasmCode) + if err != nil { + return err + } + + if err = payloadWriter(compressedWasm); err != nil { + return err + } + } + + return nil +} + +// RestoreExtension implements the snapshot.ExtensionSnapshotter interface. +// RestoreExtension is used to read data from an existing extension state snapshot into the 08-wasm module. +// The payload reader returns io.EOF when it has reached the end of the extension state snapshot. +func (ws *WasmSnapshotter) RestoreExtension(height uint64, format uint32, payloadReader snapshot.ExtensionPayloadReader) error { + if format == ws.SnapshotFormat() { + return ws.processAllItems(height, payloadReader, restoreV1) + } + + return errorsmod.Wrapf(snapshot.ErrUnknownFormat, "expected %d, got %d", ws.SnapshotFormat(), format) +} + +func restoreV1(ctx sdk.Context, k *Keeper, compressedCode []byte) error { + if !types.IsGzip(compressedCode) { + return errorsmod.Wrap(types.ErrInvalidData, "expected wasm code is not gzip format") + } + + wasmCode, err := types.Uncompress(compressedCode, types.MaxWasmByteSize()) + if err != nil { + return errorsmod.Wrap(err, "failed to uncompress wasm code") + } + + checksum, err := k.wasmVM.StoreCode(wasmCode) + if err != nil { + return errorsmod.Wrap(err, "failed to store wasm code") + } + + if err := k.wasmVM.Pin(checksum); err != nil { + return errorsmod.Wrapf(err, "failed to pin checksum: %s to in-memory cache", hex.EncodeToString(checksum)) + } + + return nil +} + +func (ws *WasmSnapshotter) processAllItems( + height uint64, + payloadReader snapshot.ExtensionPayloadReader, + cb func(sdk.Context, *Keeper, []byte) error, +) error { + ctx := sdk.NewContext(ws.cms, tmproto.Header{Height: int64(height)}, false, nil) + for { + payload, err := payloadReader() + if err == io.EOF { + break + } else if err != nil { + return err + } + + if err := cb(ctx, ws.keeper, payload); err != nil { + return errorsmod.Wrap(err, "failure processing snapshot item") + } + } + + return nil +} diff --git a/modules/light-clients/08-wasm/keeper/snapshotter_test.go b/modules/light-clients/08-wasm/keeper/snapshotter_test.go new file mode 100644 index 00000000000..6cb6d81e769 --- /dev/null +++ b/modules/light-clients/08-wasm/keeper/snapshotter_test.go @@ -0,0 +1,121 @@ +package keeper_test + +import ( + "encoding/hex" + "time" + + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + + wasmtesting "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing/simapp" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" +) + +func (suite *KeeperTestSuite) TestSnapshotter() { + gzippedContract, err := types.GzipIt(append(wasmtesting.WasmMagicNumber, []byte("gzipped-contract")...)) + suite.Require().NoError(err) + + testCases := []struct { + name string + contracts [][]byte + }{ + { + name: "single contract", + contracts: [][]byte{wasmtesting.Code}, + }, + { + name: "multiple contracts", + contracts: [][]byte{wasmtesting.Code, gzippedContract}, + }, + } + + for _, tc := range testCases { + tc := tc + + suite.Run(tc.name, func() { + t := suite.T() + wasmClientApp := suite.SetupSnapshotterWithMockVM() + + ctx := wasmClientApp.NewUncachedContext(false, tmproto.Header{ + ChainID: "foo", + Height: wasmClientApp.LastBlockHeight() + 1, + Time: time.Now(), + }) + + var srcChecksumCodes []byte + var checksums [][]byte + // store contract on chain + for _, contract := range tc.contracts { + signer := authtypes.NewModuleAddress(govtypes.ModuleName).String() + msg := types.NewMsgStoreCode(signer, contract) + + res, err := wasmClientApp.WasmClientKeeper.StoreCode(ctx, msg) + suite.Require().NoError(err) + + checksums = append(checksums, res.Checksum) + srcChecksumCodes = append(srcChecksumCodes, res.Checksum...) + + suite.Require().NoError(err) + } + + // create snapshot + res, err := wasmClientApp.Commit() + suite.Require().NoError(err) + suite.Require().NotNil(res) + + snapshotHeight := uint64(wasmClientApp.LastBlockHeight()) + snapshot, err := wasmClientApp.SnapshotManager().Create(snapshotHeight) + suite.Require().NoError(err) + suite.Require().NotNil(snapshot) + + // setup dest app with snapshot imported + destWasmClientApp := simapp.SetupWithEmptyStore(t, suite.mockVM) + destCtx := destWasmClientApp.NewUncachedContext(false, tmproto.Header{ + ChainID: "bar", + Height: destWasmClientApp.LastBlockHeight() + 1, + Time: time.Now(), + }) + + resp, err := destWasmClientApp.WasmClientKeeper.Checksums(destCtx, &types.QueryChecksumsRequest{}) + suite.Require().NoError(err) + suite.Require().Empty(resp.Checksums) + + suite.Require().NoError(destWasmClientApp.SnapshotManager().Restore(*snapshot)) + + for i := uint32(0); i < snapshot.Chunks; i++ { + chunkBz, err := wasmClientApp.SnapshotManager().LoadChunk(snapshot.Height, snapshot.Format, i) + suite.Require().NoError(err) + + end, err := destWasmClientApp.SnapshotManager().RestoreChunk(chunkBz) + suite.Require().NoError(err) + + if end { + break + } + } + + var allDestAppChecksumsInWasmVMStore []byte + // check wasm contracts are imported + ctx = destWasmClientApp.NewUncachedContext(false, tmproto.Header{ + ChainID: "foo", + Height: destWasmClientApp.LastBlockHeight() + 1, + Time: time.Now(), + }) + + for _, checksum := range checksums { + resp, err := destWasmClientApp.WasmClientKeeper.Code(ctx, &types.QueryCodeRequest{Checksum: hex.EncodeToString(checksum)}) + suite.Require().NoError(err) + + checksum, err := types.CreateChecksum(resp.Data) + suite.Require().NoError(err) + + allDestAppChecksumsInWasmVMStore = append(allDestAppChecksumsInWasmVMStore, checksum...) + } + + suite.Require().Equal(srcChecksumCodes, allDestAppChecksumsInWasmVMStore) + }) + } +} diff --git a/modules/light-clients/08-wasm/testing/mock_engine.go b/modules/light-clients/08-wasm/testing/mock_engine.go new file mode 100644 index 00000000000..82191bdbf42 --- /dev/null +++ b/modules/light-clients/08-wasm/testing/mock_engine.go @@ -0,0 +1,268 @@ +package testing + +import ( + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "reflect" + + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" +) + +const DefaultGasUsed = uint64(1) + +var ( + _ ibcwasm.WasmEngine = (*MockWasmEngine)(nil) + + // queryTypes contains all the possible query message types. + queryTypes = [...]any{types.StatusMsg{}, types.ExportMetadataMsg{}, types.TimestampAtHeightMsg{}, types.VerifyClientMessageMsg{}, types.CheckForMisbehaviourMsg{}} + + // sudoTypes contains all the possible sudo message types. + sudoTypes = [...]any{types.UpdateStateMsg{}, types.UpdateStateOnMisbehaviourMsg{}, types.VerifyUpgradeAndUpdateStateMsg{}, types.VerifyMembershipMsg{}, types.VerifyNonMembershipMsg{}, types.MigrateClientStoreMsg{}} +) + +type ( + // queryFn is a callback function that is invoked when a specific query message type is received. + queryFn 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) ([]byte, uint64, error) + + // sudoFn is a callback function that is invoked when a specific sudo message type is received. + sudoFn func(checksum wasmvm.Checksum, env wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) +) + +func NewMockWasmEngine() *MockWasmEngine { + m := &MockWasmEngine{ + queryCallbacks: map[string]queryFn{}, + sudoCallbacks: map[string]sudoFn{}, + storedContracts: map[uint32][]byte{}, + } + + for _, msgType := range queryTypes { + typeName := reflect.TypeOf(msgType).Name() + m.queryCallbacks[typeName] = 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) ([]byte, uint64, error) { + panic(fmt.Errorf("no callback specified for type %s", typeName)) + } + } + + for _, msgType := range sudoTypes { + typeName := reflect.TypeOf(msgType).Name() + m.sudoCallbacks[typeName] = func(checksum wasmvm.Checksum, env wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + panic(fmt.Errorf("no callback specified for type %s", typeName)) + } + } + + // Set up default behavior for Store/Pin/Get + m.StoreCodeFn = func(code wasmvm.WasmCode) (wasmvm.Checksum, error) { + checkSum, _ := types.CreateChecksum(code) + + m.storedContracts[binary.LittleEndian.Uint32(checkSum)] = code + return checkSum, nil + } + + m.PinFn = func(checksum wasmvm.Checksum) error { + return nil + } + + m.UnpinFn = func(checksum wasmvm.Checksum) error { + return nil + } + + m.GetCodeFn = func(checksum wasmvm.Checksum) (wasmvm.WasmCode, error) { + code, ok := m.storedContracts[binary.LittleEndian.Uint32(checksum)] + if !ok { + return nil, errors.New("code not found") + } + return code, nil + } + + return m +} + +// RegisterQueryCallback registers a callback for a specific message type. +func (m *MockWasmEngine) RegisterQueryCallback(queryMessage any, fn queryFn) { + typeName := reflect.TypeOf(queryMessage).Name() + if _, found := m.queryCallbacks[typeName]; !found { + panic(fmt.Errorf("unexpected argument of type %s passed", typeName)) + } + m.queryCallbacks[typeName] = fn +} + +// RegisterSudoCallback registers a callback for a specific sudo message type. +func (m *MockWasmEngine) RegisterSudoCallback(sudoMessage any, fn sudoFn) { + typeName := reflect.TypeOf(sudoMessage).Name() + if _, found := m.sudoCallbacks[typeName]; !found { + panic(fmt.Errorf("unexpected argument of type %s passed", typeName)) + } + m.sudoCallbacks[typeName] = fn +} + +// MockWasmEngine implements types.WasmEngine for testing purpose. One or multiple messages can be stubbed. +// Without a stub function a panic is thrown. +// ref: https://github.com/CosmWasm/wasmd/blob/v0.42.0/x/wasm/keeper/wasmtesting/mock_engine.go#L19 +type MockWasmEngine struct { + StoreCodeFn func(code wasmvm.WasmCode) (wasmvm.Checksum, error) + 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.Response, uint64, error) + MigrateFn func(checksum wasmvm.Checksum, env wasmvmtypes.Env, migrateMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) + GetCodeFn func(checksum wasmvm.Checksum) (wasmvm.WasmCode, error) + PinFn func(checksum wasmvm.Checksum) error + UnpinFn func(checksum wasmvm.Checksum) error + + // queryCallbacks contains a mapping of queryMsg field type name to callback function. + queryCallbacks map[string]queryFn + sudoCallbacks map[string]sudoFn + + // contracts contains a mapping of checksum to code. + storedContracts map[uint32][]byte +} + +// StoreCode implements the WasmEngine interface. +func (m *MockWasmEngine) StoreCode(code wasmvm.WasmCode) (wasmvm.Checksum, error) { + if m.StoreCodeFn == nil { + panic("mock engine is not properly initialized") + } + return m.StoreCodeFn(code) +} + +// Instantiate implements the WasmEngine interface. +func (m *MockWasmEngine) Instantiate(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.Response, uint64, error) { + if m.InstantiateFn == nil { + panic("mock engine is not properly initialized") + } + return m.InstantiateFn(checksum, env, info, initMsg, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +// Query implements the WasmEngine interface. +func (m *MockWasmEngine) Query(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) ([]byte, uint64, error) { + msgTypeName := getQueryMsgPayloadTypeName(queryMsg) + + callbackFn, ok := m.queryCallbacks[msgTypeName] + if !ok { + panic(fmt.Errorf("no callback specified for %s", msgTypeName)) + } + + return callbackFn(checksum, env, queryMsg, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +// Migrate implements the WasmEngine interface. +func (m *MockWasmEngine) Migrate(checksum wasmvm.Checksum, env wasmvmtypes.Env, migrateMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + if m.MigrateFn == nil { + panic("mock engine is not properly initialized") + } + return m.MigrateFn(checksum, env, migrateMsg, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +// Sudo implements the WasmEngine interface. +func (m *MockWasmEngine) Sudo(checksum wasmvm.Checksum, env wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, goapi wasmvm.GoAPI, querier wasmvm.Querier, gasMeter wasmvm.GasMeter, gasLimit uint64, deserCost wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + msgTypeName := getSudoMsgPayloadTypeName(sudoMsg) + + sudoFn, ok := m.sudoCallbacks[msgTypeName] + if !ok { + panic(fmt.Errorf("no callback specified for %s", msgTypeName)) + } + + return sudoFn(checksum, env, sudoMsg, store, goapi, querier, gasMeter, gasLimit, deserCost) +} + +// GetCode implements the WasmEngine interface. +func (m *MockWasmEngine) GetCode(checksum wasmvm.Checksum) (wasmvm.WasmCode, error) { + if m.GetCodeFn == nil { + panic("mock engine is not properly initialized") + } + return m.GetCodeFn(checksum) +} + +// Pin implements the WasmEngine interface. +func (m *MockWasmEngine) Pin(checksum wasmvm.Checksum) error { + if m.PinFn == nil { + panic("mock engine is not properly initialized") + } + return m.PinFn(checksum) +} + +// Unpin implements the WasmEngine interface. +func (m *MockWasmEngine) Unpin(checksum wasmvm.Checksum) error { + if m.UnpinFn == nil { + panic("mock engine is not properly initialized") + } + return m.UnpinFn(checksum) +} + +// getQueryMsgPayloadTypeName extracts the name of the struct that is populated. +// this value is used as a key to map to a callback function to handle that message type. +func getQueryMsgPayloadTypeName(queryMsgBz []byte) string { + payload := types.QueryMsg{} + if err := json.Unmarshal(queryMsgBz, &payload); err != nil { + panic(err) + } + + var payloadField any + if payload.Status != nil { + payloadField = *payload.Status + } + + if payload.CheckForMisbehaviour != nil { + payloadField = *payload.CheckForMisbehaviour + } + + if payload.ExportMetadata != nil { + payloadField = *payload.ExportMetadata + } + + if payload.TimestampAtHeight != nil { + payloadField = *payload.TimestampAtHeight + } + + if payload.VerifyClientMessage != nil { + payloadField = *payload.VerifyClientMessage + } + + if payloadField == nil { + panic(fmt.Errorf("failed to extract valid query message from bytes: %s", string(queryMsgBz))) + } + + return reflect.TypeOf(payloadField).Name() +} + +// getSudoMsgPayloadTypeName extracts the name of the struct that is populated. +// this value is used as a key to map to a callback function to handle that message type. +func getSudoMsgPayloadTypeName(sudoMsgBz []byte) string { + payload := types.SudoMsg{} + if err := json.Unmarshal(sudoMsgBz, &payload); err != nil { + panic(err) + } + + var payloadField any + if payload.UpdateState != nil { + payloadField = *payload.UpdateState + } + + if payload.UpdateStateOnMisbehaviour != nil { + payloadField = *payload.UpdateStateOnMisbehaviour + } + + if payload.VerifyUpgradeAndUpdateState != nil { + payloadField = *payload.VerifyUpgradeAndUpdateState + } + + if payload.VerifyMembership != nil { + payloadField = *payload.VerifyMembership + } + + if payload.VerifyNonMembership != nil { + payloadField = *payload.VerifyNonMembership + } + + if payload.MigrateClientStore != nil { + payloadField = *payload.MigrateClientStore + } + + if payloadField == nil { + panic(fmt.Errorf("failed to extract valid sudo message from bytes: %s", string(sudoMsgBz))) + } + + return reflect.TypeOf(payloadField).Name() +} diff --git a/modules/light-clients/08-wasm/testing/values.go b/modules/light-clients/08-wasm/testing/values.go new file mode 100644 index 00000000000..dd54a4e3f3b --- /dev/null +++ b/modules/light-clients/08-wasm/testing/values.go @@ -0,0 +1,33 @@ +package testing + +import ( + "errors" + + "github.com/cosmos/cosmos-sdk/codec" + + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" +) + +var ( + // Represents the code of the wasm contract used in the tests with a mock vm. + WasmMagicNumber = []byte("\x00\x61\x73\x6D") + Code = append(WasmMagicNumber, []byte("0123456780123456780123456780")...) + contractClientState = []byte{1} + contractConsensusState = []byte{2} + MockClientStateBz = []byte("client-state-data") + MockConsensusStateBz = []byte("consensus-state-data") + MockValidProofBz = []byte("valid proof") + MockInvalidProofBz = []byte("invalid proof") + MockUpgradedClientStateProofBz = []byte("upgraded client state proof") + MockUpgradedConsensusStateProofBz = []byte("upgraded consensus state proof") + + ErrMockContract = errors.New("mock contract error") + ErrMockVM = errors.New("mock vm error") +) + +// CreateMockClientStateBz returns valid client state bytes for use in tests. +func CreateMockClientStateBz(cdc codec.BinaryCodec, checksum types.Checksum) []byte { + mockClientSate := types.NewClientState([]byte{1}, checksum, clienttypes.NewHeight(2000, 2)) + return clienttypes.MustMarshalClientState(cdc, mockClientSate) +} diff --git a/modules/light-clients/08-wasm/testing/wasm_endpoint.go b/modules/light-clients/08-wasm/testing/wasm_endpoint.go new file mode 100644 index 00000000000..9a125051ed6 --- /dev/null +++ b/modules/light-clients/08-wasm/testing/wasm_endpoint.go @@ -0,0 +1,49 @@ +package testing + +import ( + "github.com/stretchr/testify/require" + + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +// WasmEndpoint is a wrapper around the ibctesting pkg Endpoint struct. +// It will override any functions which require special handling for the wasm client. +type WasmEndpoint struct { + *ibctesting.Endpoint +} + +// NewWasmEndpoint returns a wasm endpoint with the default ibctesting pkg +// Endpoint embedded. +func NewWasmEndpoint(chain *ibctesting.TestChain) *WasmEndpoint { + return &WasmEndpoint{ + Endpoint: ibctesting.NewDefaultEndpoint(chain), + } +} + +// CreateClient creates an wasm client on a mock cometbft chain. +// The client and consensus states are represented by byte slices +// and the starting height is 1. +func (endpoint *WasmEndpoint) CreateClient() error { + checksum, err := types.CreateChecksum(Code) + require.NoError(endpoint.Chain.TB, err) + + clientState := types.NewClientState(contractClientState, checksum, clienttypes.NewHeight(0, 1)) + consensusState := types.NewConsensusState(contractConsensusState, 0) + + msg, err := clienttypes.NewMsgCreateClient( + clientState, consensusState, endpoint.Chain.SenderAccount.GetAddress().String(), + ) + require.NoError(endpoint.Chain.TB, err) + + res, err := endpoint.Chain.SendMsgs(msg) + if err != nil { + return err + } + + endpoint.ClientID, err = ibctesting.ParseClientIDFromEvents(res.Events) + require.NoError(endpoint.Chain.TB, err) + + return nil +} diff --git a/modules/light-clients/08-wasm/types/client_state_test.go b/modules/light-clients/08-wasm/types/client_state_test.go new file mode 100644 index 00000000000..d4a4f5410f7 --- /dev/null +++ b/modules/light-clients/08-wasm/types/client_state_test.go @@ -0,0 +1,619 @@ +package types_test + +import ( + "encoding/json" + "time" + + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + + storetypes "cosmossdk.io/store/types" + + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" + wasmtesting "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + commitmenttypes "github.com/cosmos/ibc-go/v8/modules/core/23-commitment/types" + host "github.com/cosmos/ibc-go/v8/modules/core/24-host" + ibcerrors "github.com/cosmos/ibc-go/v8/modules/core/errors" + "github.com/cosmos/ibc-go/v8/modules/core/exported" + solomachine "github.com/cosmos/ibc-go/v8/modules/light-clients/06-solomachine" + ibctesting "github.com/cosmos/ibc-go/v8/testing" + ibcmock "github.com/cosmos/ibc-go/v8/testing/mock" +) + +func (suite *TypesTestSuite) TestStatus() { + testCases := []struct { + name string + malleate func() + expStatus exported.Status + }{ + { + "client is active", + func() {}, + exported.Active, + }, + { + "client is frozen", + func() { + 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) ([]byte, uint64, error) { + resp, err := json.Marshal(types.StatusResult{Status: exported.Frozen.String()}) + suite.Require().NoError(err) + return resp, wasmtesting.DefaultGasUsed, nil + }) + }, + exported.Frozen, + }, + { + "client status is expired", + func() { + 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) ([]byte, uint64, error) { + resp, err := json.Marshal(types.StatusResult{Status: exported.Expired.String()}) + suite.Require().NoError(err) + return resp, wasmtesting.DefaultGasUsed, nil + }) + }, + exported.Expired, + }, + { + "client status is unknown: vm returns an error", + func() { + 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) ([]byte, uint64, error) { + return nil, 0, wasmtesting.ErrMockContract + }) + }, + exported.Unknown, + }, + { + "client status is unauthorized: checksum is not stored", + func() { + err := ibcwasm.Checksums.Remove(suite.chainA.GetContext(), suite.checksum) + suite.Require().NoError(err) + }, + exported.Unauthorized, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() + + endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) + err := endpoint.CreateClient() + suite.Require().NoError(err) + + tc.malleate() + + clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), endpoint.ClientID) + clientState := endpoint.GetClientState() + + status := clientState.Status(suite.chainA.GetContext(), clientStore, suite.chainA.App.AppCodec()) + suite.Require().Equal(tc.expStatus, status) + }) + } +} + +func (suite *TypesTestSuite) TestGetTimestampAtHeight() { + var height exported.Height + + expectedTimestamp := uint64(time.Now().UnixNano()) + + testCases := []struct { + name string + malleate func() + expErr error + }{ + { + "success", + func() { + suite.mockVM.RegisterQueryCallback(types.TimestampAtHeightMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, queryMsg []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) ([]byte, uint64, error) { + var payload types.QueryMsg + err := json.Unmarshal(queryMsg, &payload) + suite.Require().NoError(err) + + suite.Require().NotNil(payload.TimestampAtHeight) + suite.Require().Nil(payload.CheckForMisbehaviour) + suite.Require().Nil(payload.Status) + suite.Require().Nil(payload.ExportMetadata) + suite.Require().Nil(payload.VerifyClientMessage) + + resp, err := json.Marshal(types.TimestampAtHeightResult{Timestamp: expectedTimestamp}) + suite.Require().NoError(err) + + return resp, wasmtesting.DefaultGasUsed, nil + }) + }, + nil, + }, + { + "failure: contract returns error", + func() { + suite.mockVM.RegisterQueryCallback(types.TimestampAtHeightMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) ([]byte, uint64, error) { + return nil, 0, wasmtesting.ErrMockContract + }) + }, + types.ErrWasmContractCallFailed, + }, + { + "error: invalid height", + func() { + height = ibcmock.Height{} + }, + ibcerrors.ErrInvalidType, + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() + + endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) + err := endpoint.CreateClient() + suite.Require().NoError(err) + + clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), endpoint.ClientID) + clientState := endpoint.GetClientState().(*types.ClientState) + height = clientState.GetLatestHeight() + + tc.malleate() + + timestamp, err := clientState.GetTimestampAtHeight(suite.chainA.GetContext(), clientStore, suite.chainA.App.AppCodec(), height) + + expPass := tc.expErr == nil + if expPass { + suite.Require().NoError(err) + suite.Require().Equal(expectedTimestamp, timestamp) + } else { + suite.Require().ErrorIs(err, tc.expErr) + } + }) + } +} + +func (suite *TypesTestSuite) TestValidate() { + testCases := []struct { + name string + clientState *types.ClientState + expPass bool + }{ + { + name: "valid client", + clientState: types.NewClientState([]byte{0}, wasmtesting.Code, clienttypes.ZeroHeight()), + expPass: true, + }, + { + name: "nil data", + clientState: types.NewClientState(nil, wasmtesting.Code, clienttypes.ZeroHeight()), + expPass: false, + }, + { + name: "empty data", + clientState: types.NewClientState([]byte{}, wasmtesting.Code, clienttypes.ZeroHeight()), + expPass: false, + }, + { + name: "nil checksum", + clientState: types.NewClientState([]byte{0}, nil, clienttypes.ZeroHeight()), + expPass: false, + }, + { + name: "empty checksum", + clientState: types.NewClientState([]byte{0}, []byte{}, clienttypes.ZeroHeight()), + expPass: false, + }, + { + name: "longer than 32 bytes checksum", + clientState: types.NewClientState( + []byte{0}, + []byte{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, + 30, 31, 32, 33, + }, + clienttypes.ZeroHeight(), + ), + expPass: false, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + err := tc.clientState.Validate() + if tc.expPass { + suite.Require().NoError(err, tc.name) + } else { + suite.Require().Error(err, tc.name) + } + }) + } +} + +func (suite *TypesTestSuite) TestInitialize() { + var ( + consensusState exported.ConsensusState + clientState exported.ClientState + clientStore storetypes.KVStore + ) + + testCases := []struct { + name string + malleate func() + expError error + }{ + { + "success: new mock client", + func() {}, + nil, + }, + { + "success: validate contract address", + func() { + suite.mockVM.InstantiateFn = func(_ wasmvm.Checksum, env wasmvmtypes.Env, _ wasmvmtypes.MessageInfo, initMsg []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + var payload types.InstantiateMessage + err := json.Unmarshal(initMsg, &payload) + suite.Require().NoError(err) + + suite.Require().Equal(env.Contract.Address, defaultWasmClientID) + + store.Set(host.ClientStateKey(), clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), payload.ClientState)) + store.Set(host.ConsensusStateKey(payload.ClientState.LatestHeight), clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), payload.ConsensusState)) + + resp, err := json.Marshal(types.EmptyResult{}) + suite.Require().NoError(err) + + return &wasmvmtypes.Response{Data: resp}, 0, nil + } + }, + nil, + }, + { + "failure: clientStore prefix does not include clientID", + func() { + clientStore = suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), ibctesting.InvalidID) + }, + types.ErrWasmContractCallFailed, + }, + { + "failure: invalid consensus state", + func() { + // set upgraded consensus state to solomachine consensus state + consensusState = &solomachine.ConsensusState{} + }, + clienttypes.ErrInvalidConsensus, + }, + { + "failure: checksum has not been stored.", + func() { + clientState = types.NewClientState([]byte{1}, []byte("unknown"), clienttypes.NewHeight(0, 1)) + }, + types.ErrInvalidChecksum, + }, + { + "failure: InstantiateFn returns error", + func() { + suite.mockVM.InstantiateFn = func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ wasmvmtypes.MessageInfo, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + return nil, 0, wasmtesting.ErrMockContract + } + }, + types.ErrWasmContractCallFailed, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() + + checksum, err := types.CreateChecksum(wasmtesting.Code) + suite.Require().NoError(err) + + clientState = types.NewClientState([]byte{1}, checksum, clienttypes.NewHeight(0, 1)) + consensusState = types.NewConsensusState([]byte{2}, 0) + + clientID := suite.chainA.App.GetIBCKeeper().ClientKeeper.GenerateClientIdentifier(suite.chainA.GetContext(), clientState.ClientType()) + clientStore = suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), clientID) + + tc.malleate() + + err = clientState.Initialize(suite.chainA.GetContext(), suite.chainA.Codec, clientStore, consensusState) + + expPass := tc.expError == nil + if expPass { + suite.Require().NoError(err) + + expClientState := clienttypes.MustMarshalClientState(suite.chainA.Codec, clientState) + suite.Require().Equal(expClientState, clientStore.Get(host.ClientStateKey())) + + expConsensusState := clienttypes.MustMarshalConsensusState(suite.chainA.Codec, consensusState) + suite.Require().Equal(expConsensusState, clientStore.Get(host.ConsensusStateKey(clientState.GetLatestHeight()))) + } else { + suite.Require().ErrorIs(err, tc.expError) + } + }) + } +} + +func (suite *TypesTestSuite) TestVerifyMembership() { + var ( + clientState exported.ClientState + expClientStateBz []byte + path exported.Path + proof []byte + proofHeight exported.Height + value []byte + ) + + testCases := []struct { + name string + malleate func() + expError error + }{ + { + "success", + func() { + expClientStateBz = GetSimApp(suite.chainA).GetIBCKeeper().ClientKeeper.MustMarshalClientState(clientState) + suite.mockVM.RegisterSudoCallback(types.VerifyMembershipMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, sudoMsg []byte, _ wasmvm.KVStore, + _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction, + ) (*wasmvmtypes.Response, uint64, error) { + var payload types.SudoMsg + err := json.Unmarshal(sudoMsg, &payload) + suite.Require().NoError(err) + + suite.Require().NotNil(payload.VerifyMembership) + suite.Require().Nil(payload.UpdateState) + suite.Require().Nil(payload.UpdateStateOnMisbehaviour) + suite.Require().Nil(payload.VerifyNonMembership) + suite.Require().Nil(payload.VerifyUpgradeAndUpdateState) + suite.Require().Equal(proofHeight, payload.VerifyMembership.Height) + suite.Require().Equal(path, payload.VerifyMembership.Path) + suite.Require().Equal(proof, payload.VerifyMembership.Proof) + suite.Require().Equal(value, payload.VerifyMembership.Value) + + bz, err := json.Marshal(types.EmptyResult{}) + suite.Require().NoError(err) + + return &wasmvmtypes.Response{Data: bz}, wasmtesting.DefaultGasUsed, nil + }) + }, + nil, + }, + { + "success: with update client state", + func() { + suite.mockVM.RegisterSudoCallback(types.VerifyMembershipMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, + _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction, + ) (*wasmvmtypes.Response, uint64, error) { + var payload types.SudoMsg + err := json.Unmarshal(sudoMsg, &payload) + suite.Require().NoError(err) + + suite.Require().NotNil(payload.VerifyMembership) + suite.Require().Nil(payload.UpdateState) + suite.Require().Nil(payload.UpdateStateOnMisbehaviour) + suite.Require().Nil(payload.VerifyNonMembership) + suite.Require().Nil(payload.VerifyUpgradeAndUpdateState) + suite.Require().Equal(proofHeight, payload.VerifyMembership.Height) + suite.Require().Equal(path, payload.VerifyMembership.Path) + suite.Require().Equal(proof, payload.VerifyMembership.Proof) + suite.Require().Equal(value, payload.VerifyMembership.Value) + + bz, err := json.Marshal(types.EmptyResult{}) + suite.Require().NoError(err) + + expClientStateBz = wasmtesting.CreateMockClientStateBz(suite.chainA.Codec, suite.checksum) + store.Set(host.ClientStateKey(), expClientStateBz) + + return &wasmvmtypes.Response{Data: bz}, wasmtesting.DefaultGasUsed, nil + }) + }, + nil, + }, + { + "wasm vm returns invalid proof error", + func() { + proof = wasmtesting.MockInvalidProofBz + + suite.mockVM.RegisterSudoCallback(types.VerifyMembershipMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, + _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction, + ) (*wasmvmtypes.Response, uint64, error) { + return nil, wasmtesting.DefaultGasUsed, commitmenttypes.ErrInvalidProof + }) + }, + types.ErrWasmContractCallFailed, + }, + { + "proof height greater than client state latest height", + func() { + proofHeight = clienttypes.NewHeight(1, 100) + }, + ibcerrors.ErrInvalidHeight, + }, + { + "invalid path argument", + func() { + path = ibcmock.KeyPath{} + }, + ibcerrors.ErrInvalidType, + }, + { + "proof height is invalid type", + func() { + proofHeight = ibcmock.Height{} + }, + ibcerrors.ErrInvalidType, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() + + endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) + err := endpoint.CreateClient() + suite.Require().NoError(err) + + path = commitmenttypes.NewMerklePath("/ibc/key/path") + proof = wasmtesting.MockValidProofBz + proofHeight = clienttypes.NewHeight(0, 1) + value = []byte("value") + + clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), endpoint.ClientID) + clientState = endpoint.GetClientState() + + tc.malleate() + + err = clientState.VerifyMembership(suite.chainA.GetContext(), clientStore, suite.chainA.Codec, proofHeight, 0, 0, proof, path, value) + + expPass := tc.expError == nil + if expPass { + suite.Require().NoError(err) + + clientStateBz := clientStore.Get(host.ClientStateKey()) + suite.Require().Equal(expClientStateBz, clientStateBz) + } else { + suite.Require().ErrorIs(err, tc.expError, "unexpected error in VerifyMembership") + } + }) + } +} + +func (suite *TypesTestSuite) TestVerifyNonMembership() { + var ( + clientState exported.ClientState + expClientStateBz []byte + path exported.Path + proof []byte + proofHeight exported.Height + ) + + testCases := []struct { + name string + malleate func() + expError error + }{ + { + "success", + func() { + expClientStateBz = GetSimApp(suite.chainA).GetIBCKeeper().ClientKeeper.MustMarshalClientState(clientState) + suite.mockVM.RegisterSudoCallback(types.VerifyNonMembershipMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, sudoMsg []byte, _ wasmvm.KVStore, + _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction, + ) (*wasmvmtypes.Response, uint64, error) { + var payload types.SudoMsg + err := json.Unmarshal(sudoMsg, &payload) + suite.Require().NoError(err) + + suite.Require().NotNil(payload.VerifyNonMembership) + suite.Require().Nil(payload.UpdateState) + suite.Require().Nil(payload.UpdateStateOnMisbehaviour) + suite.Require().Nil(payload.VerifyMembership) + suite.Require().Nil(payload.VerifyUpgradeAndUpdateState) + suite.Require().Equal(proofHeight, payload.VerifyNonMembership.Height) + suite.Require().Equal(path, payload.VerifyNonMembership.Path) + suite.Require().Equal(proof, payload.VerifyNonMembership.Proof) + + bz, err := json.Marshal(types.EmptyResult{}) + suite.Require().NoError(err) + + return &wasmvmtypes.Response{Data: bz}, wasmtesting.DefaultGasUsed, nil + }) + }, + nil, + }, + { + "success: with update client state", + func() { + suite.mockVM.RegisterSudoCallback(types.VerifyNonMembershipMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, + _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction, + ) (*wasmvmtypes.Response, uint64, error) { + var payload types.SudoMsg + err := json.Unmarshal(sudoMsg, &payload) + suite.Require().NoError(err) + + suite.Require().NotNil(payload.VerifyNonMembership) + suite.Require().Nil(payload.UpdateState) + suite.Require().Nil(payload.UpdateStateOnMisbehaviour) + suite.Require().Nil(payload.VerifyMembership) + suite.Require().Nil(payload.VerifyUpgradeAndUpdateState) + suite.Require().Equal(proofHeight, payload.VerifyNonMembership.Height) + suite.Require().Equal(path, payload.VerifyNonMembership.Path) + suite.Require().Equal(proof, payload.VerifyNonMembership.Proof) + + bz, err := json.Marshal(types.EmptyResult{}) + suite.Require().NoError(err) + + expClientStateBz = wasmtesting.CreateMockClientStateBz(suite.chainA.Codec, suite.checksum) + store.Set(host.ClientStateKey(), expClientStateBz) + + return &wasmvmtypes.Response{Data: bz}, wasmtesting.DefaultGasUsed, nil + }) + }, + nil, + }, + { + "wasm vm returns invalid proof error", + func() { + proof = wasmtesting.MockInvalidProofBz + + suite.mockVM.RegisterSudoCallback(types.VerifyNonMembershipMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, + _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction, + ) (*wasmvmtypes.Response, uint64, error) { + return nil, wasmtesting.DefaultGasUsed, commitmenttypes.ErrInvalidProof + }) + }, + types.ErrWasmContractCallFailed, + }, + { + "proof height greater than client state latest height", + func() { + proofHeight = clienttypes.NewHeight(1, 100) + }, + ibcerrors.ErrInvalidHeight, + }, + { + "invalid path argument", + func() { + path = ibcmock.KeyPath{} + }, + ibcerrors.ErrInvalidType, + }, + { + "proof height is invalid type", + func() { + proofHeight = ibcmock.Height{} + }, + ibcerrors.ErrInvalidType, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() + + endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) + err := endpoint.CreateClient() + suite.Require().NoError(err) + + path = commitmenttypes.NewMerklePath("/ibc/key/path") + proof = wasmtesting.MockInvalidProofBz + proofHeight = clienttypes.NewHeight(0, 1) + + clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), endpoint.ClientID) + clientState = endpoint.GetClientState() + + tc.malleate() + + err = clientState.VerifyNonMembership(suite.chainA.GetContext(), clientStore, suite.chainA.Codec, proofHeight, 0, 0, proof, path) + + expPass := tc.expError == nil + if expPass { + suite.Require().NoError(err) + + clientStateBz := clientStore.Get(host.ClientStateKey()) + suite.Require().Equal(expClientStateBz, clientStateBz) + } else { + suite.Require().ErrorIs(err, tc.expError, "unexpected error in VerifyNonMembership") + } + }) + } +} diff --git a/modules/light-clients/08-wasm/types/migrate_contract_test.go b/modules/light-clients/08-wasm/types/migrate_contract_test.go new file mode 100644 index 00000000000..fbaeb4deed3 --- /dev/null +++ b/modules/light-clients/08-wasm/types/migrate_contract_test.go @@ -0,0 +1,141 @@ +package types_test + +import ( + "encoding/hex" + "encoding/json" + + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" + wasmtesting "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + host "github.com/cosmos/ibc-go/v8/modules/core/24-host" +) + +func (suite *TypesTestSuite) TestMigrateContract() { + var ( + oldHash []byte + newHash []byte + payload []byte + expClientState *types.ClientState + ) + + testCases := []struct { + name string + malleate func() + expErr error + }{ + { + "success: no update to client state", + func() { + err := ibcwasm.Checksums.Set(suite.chainA.GetContext(), newHash) + suite.Require().NoError(err) + + payload = []byte{1} + expChecksum := wasmvmtypes.ForceNewChecksum(hex.EncodeToString(newHash)) + + suite.mockVM.MigrateFn = func(checksum wasmvm.Checksum, env wasmvmtypes.Env, msg []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { + suite.Require().Equal(expChecksum, checksum) + suite.Require().Equal(defaultWasmClientID, env.Contract.Address) + suite.Require().Equal(payload, msg) + + data, err := json.Marshal(types.EmptyResult{}) + suite.Require().NoError(err) + + return &wasmvmtypes.Response{Data: data}, wasmtesting.DefaultGasUsed, nil + } + }, + nil, + }, + { + "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.Response, 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.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.Response, uint64, error) { + panic("unreachable") + } + }, + types.ErrWasmCodeExists, + }, + { + "failure: checksum not found", + func() { + err := ibcwasm.Checksums.Remove(suite.chainA.GetContext(), newHash) + suite.Require().NoError(err) + }, + types.ErrWasmChecksumNotFound, + }, + { + "failure: contract returns error", + func() { + err := ibcwasm.Checksums.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.Response, uint64, error) { + return nil, wasmtesting.DefaultGasUsed, wasmtesting.ErrMockContract + } + }, + types.ErrWasmContractCallFailed, + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() + + var err error + oldHash, err = types.CreateChecksum(wasmtesting.Code) + suite.Require().NoError(err) + newHash, err = types.CreateChecksum(append(wasmtesting.WasmMagicNumber, []byte{1, 2, 3}...)) + suite.Require().NoError(err) + + err = ibcwasm.Checksums.Set(suite.chainA.GetContext(), newHash) + suite.Require().NoError(err) + + endpointA := wasmtesting.NewWasmEndpoint(suite.chainA) + err = endpointA.CreateClient() + suite.Require().NoError(err) + + clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), endpointA.ClientID) + clientState, ok := endpointA.GetClientState().(*types.ClientState) + suite.Require().True(ok) + + expClientState = clientState + + tc.malleate() + + err = clientState.MigrateContract(suite.chainA.GetContext(), suite.chainA.App.AppCodec(), clientStore, endpointA.ClientID, newHash, payload) + + // updated client state + clientState, ok = endpointA.GetClientState().(*types.ClientState) + suite.Require().True(ok) + + expPass := tc.expErr == nil + if expPass { + suite.Require().NoError(err) + suite.Require().Equal(expClientState, clientState) + } else { + suite.Require().ErrorIs(err, tc.expErr) + } + }) + } +} diff --git a/modules/light-clients/08-wasm/types/msgs_test.go b/modules/light-clients/08-wasm/types/msgs_test.go new file mode 100644 index 00000000000..f63a6190e2f --- /dev/null +++ b/modules/light-clients/08-wasm/types/msgs_test.go @@ -0,0 +1,268 @@ +package types_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + errorsmod "cosmossdk.io/errors" + + sdk "github.com/cosmos/cosmos-sdk/types" + + wasmtesting "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + host "github.com/cosmos/ibc-go/v8/modules/core/24-host" + ibcerrors "github.com/cosmos/ibc-go/v8/modules/core/errors" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +func TestMsgStoreCodeValidateBasic(t *testing.T) { + signer := sdk.AccAddress(ibctesting.TestAccAddress).String() + testCases := []struct { + name string + msg *types.MsgStoreCode + expErr error + }{ + { + "success: valid signer address, valid length code", + types.NewMsgStoreCode(signer, wasmtesting.Code), + nil, + }, + { + "failure: code is empty", + types.NewMsgStoreCode(signer, []byte("")), + types.ErrWasmEmptyCode, + }, + { + "failure: code is too large", + types.NewMsgStoreCode(signer, make([]byte, types.MaxWasmSize+1)), + types.ErrWasmCodeTooLarge, + }, + { + "failure: signer is invalid", + types.NewMsgStoreCode("invalid", wasmtesting.Code), + ibcerrors.ErrInvalidAddress, + }, + } + + for _, tc := range testCases { + tc := tc + + err := tc.msg.ValidateBasic() + expPass := tc.expErr == nil + if expPass { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, tc.expErr) + } + } +} + +func (suite *TypesTestSuite) TestMsgStoreCodeGetSigners() { + testCases := []struct { + name string + address sdk.AccAddress + expPass bool + }{ + {"success: valid address", sdk.AccAddress(ibctesting.TestAccAddress), true}, + {"failure: nil address", nil, false}, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() + + address := tc.address + msg := types.NewMsgStoreCode(address.String(), wasmtesting.Code) + + signers, _, err := GetSimApp(suite.chainA).AppCodec().GetMsgV1Signers(msg) + if tc.expPass { + suite.Require().NoError(err) + suite.Require().Equal(address.Bytes(), signers[0]) + } else { + suite.Require().Error(err) + } + }) + } +} + +func TestMsgMigrateContractValidateBasic(t *testing.T) { + signer := sdk.AccAddress(ibctesting.TestAccAddress).String() + validChecksum, err := types.CreateChecksum(wasmtesting.Code) + require.NoError(t, err, t.Name()) + validMigrateMsg := []byte("{}") + + testCases := []struct { + name string + msg *types.MsgMigrateContract + expErr error + }{ + { + "success: valid signer address, valid checksum, valid migrate msg", + types.NewMsgMigrateContract(signer, defaultWasmClientID, validChecksum, validMigrateMsg), + nil, + }, + { + "failure: invalid signer address", + types.NewMsgMigrateContract(ibctesting.InvalidID, defaultWasmClientID, validChecksum, validMigrateMsg), + ibcerrors.ErrInvalidAddress, + }, + { + "failure: clientID is not a valid client identifier", + types.NewMsgMigrateContract(signer, ibctesting.InvalidID, validChecksum, validMigrateMsg), + host.ErrInvalidID, + }, + { + "failure: clientID is not a wasm client identifier", + types.NewMsgMigrateContract(signer, ibctesting.FirstClientID, validChecksum, validMigrateMsg), + host.ErrInvalidID, + }, + { + "failure: checksum is nil", + types.NewMsgMigrateContract(signer, defaultWasmClientID, nil, validMigrateMsg), + errorsmod.Wrap(types.ErrInvalidChecksum, "checksum cannot be empty"), + }, + { + "failure: checksum is empty", + types.NewMsgMigrateContract(signer, defaultWasmClientID, []byte{}, validMigrateMsg), + errorsmod.Wrap(types.ErrInvalidChecksum, "checksum cannot be empty"), + }, + { + "failure: checksum is not 32 bytes", + types.NewMsgMigrateContract(signer, defaultWasmClientID, []byte{1}, validMigrateMsg), + errorsmod.Wrapf(types.ErrInvalidChecksum, "expected length of 32 bytes, got %d", 1), + }, + { + "failure: migrateMsg is nil", + types.NewMsgMigrateContract(signer, defaultWasmClientID, validChecksum, nil), + errorsmod.Wrap(ibcerrors.ErrInvalidRequest, "migrate message cannot be empty"), + }, + { + "failure: migrateMsg is empty", + types.NewMsgMigrateContract(signer, defaultWasmClientID, validChecksum, []byte("")), + errorsmod.Wrap(ibcerrors.ErrInvalidRequest, "migrate message cannot be empty"), + }, + } + + for _, tc := range testCases { + tc := tc + + err := tc.msg.ValidateBasic() + expPass := tc.expErr == nil + if expPass { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, tc.expErr, tc.name) + } + } +} + +func (suite *TypesTestSuite) TestMsgMigrateContractGetSigners() { + checksum, err := types.CreateChecksum(wasmtesting.Code) + suite.Require().NoError(err) + + testCases := []struct { + name string + address sdk.AccAddress + expPass bool + }{ + {"success: valid address", sdk.AccAddress(ibctesting.TestAccAddress), true}, + {"failure: nil address", nil, false}, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() + + address := tc.address + msg := types.NewMsgMigrateContract(address.String(), defaultWasmClientID, checksum, []byte("{}")) + + signers, _, err := GetSimApp(suite.chainA).AppCodec().GetMsgV1Signers(msg) + if tc.expPass { + suite.Require().NoError(err) + suite.Require().Equal(address.Bytes(), signers[0]) + } else { + suite.Require().Error(err) + } + }) + } +} + +func TestMsgRemoveChecksumValidateBasic(t *testing.T) { + signer := sdk.AccAddress(ibctesting.TestAccAddress).String() + checksum, err := types.CreateChecksum(wasmtesting.Code) + require.NoError(t, err, t.Name()) + + testCases := []struct { + name string + msg *types.MsgRemoveChecksum + expErr error + }{ + { + "success: valid signer address, valid length checksum", + types.NewMsgRemoveChecksum(signer, checksum), + nil, + }, + { + "failure: checksum is empty", + types.NewMsgRemoveChecksum(signer, []byte("")), + types.ErrInvalidChecksum, + }, + { + "failure: checksum is nil", + types.NewMsgRemoveChecksum(signer, nil), + types.ErrInvalidChecksum, + }, + { + "failure: signer is invalid", + types.NewMsgRemoveChecksum(ibctesting.InvalidID, checksum), + ibcerrors.ErrInvalidAddress, + }, + } + + for _, tc := range testCases { + tc := tc + + err := tc.msg.ValidateBasic() + + if tc.expErr == nil { + require.NoError(t, err, tc.name) + } else { + require.ErrorIs(t, err, tc.expErr, tc.name) + } + } +} + +func (suite *TypesTestSuite) TestMsgRemoveChecksumGetSigners() { + checksum, err := types.CreateChecksum(wasmtesting.Code) + suite.Require().NoError(err) + + testCases := []struct { + name string + address sdk.AccAddress + expPass bool + }{ + {"success: valid address", sdk.AccAddress(ibctesting.TestAccAddress), true}, + {"failure: nil address", nil, false}, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() + + address := tc.address + msg := types.NewMsgRemoveChecksum(address.String(), checksum) + + signers, _, err := GetSimApp(suite.chainA).AppCodec().GetMsgV1Signers(msg) + if tc.expPass { + suite.Require().NoError(err) + suite.Require().Equal(address.Bytes(), signers[0]) + } else { + suite.Require().Error(err) + } + }) + } +} diff --git a/modules/light-clients/08-wasm/types/types_test.go b/modules/light-clients/08-wasm/types/types_test.go new file mode 100644 index 00000000000..8f882f64aaa --- /dev/null +++ b/modules/light-clients/08-wasm/types/types_test.go @@ -0,0 +1,124 @@ +package types_test + +import ( + "encoding/json" + "errors" + "testing" + + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + dbm "github.com/cosmos/cosmos-db" + testifysuite "github.com/stretchr/testify/suite" + + "cosmossdk.io/log" + storetypes "cosmossdk.io/store/types" + + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + + wasmtesting "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing" + simapp "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing/simapp" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + host "github.com/cosmos/ibc-go/v8/modules/core/24-host" + "github.com/cosmos/ibc-go/v8/modules/core/exported" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +const ( + tmClientID = "07-tendermint-0" + defaultWasmClientID = "08-wasm-0" +) + +type TypesTestSuite struct { + testifysuite.Suite + coordinator *ibctesting.Coordinator + chainA *ibctesting.TestChain + mockVM *wasmtesting.MockWasmEngine + + checksum types.Checksum +} + +func TestWasmTestSuite(t *testing.T) { + testifysuite.Run(t, new(TypesTestSuite)) +} + +func (suite *TypesTestSuite) SetupTest() { + ibctesting.DefaultTestingAppInit = setupTestingApp + + suite.coordinator = ibctesting.NewCoordinator(suite.T(), 1) + suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(1)) +} + +func init() { + ibctesting.DefaultTestingAppInit = setupTestingApp +} + +// 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 +} + +// 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.NewSimApp(log.NewNopLogger(), db, nil, true, simtestutil.EmptyAppOptions{}, nil) + return app, app.DefaultGenesis() +} + +// SetupWasmWithMockVM sets up mock cometbft chain with a mock vm. +func (suite *TypesTestSuite) SetupWasmWithMockVM() { + ibctesting.DefaultTestingAppInit = suite.setupWasmWithMockVM + + suite.coordinator = ibctesting.NewCoordinator(suite.T(), 1) + suite.chainA = suite.coordinator.GetChain(ibctesting.GetChainID(1)) + suite.checksum = storeWasmCode(suite, wasmtesting.Code) +} + +func (suite *TypesTestSuite) 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.Response, uint64, error) { + var payload types.InstantiateMessage + err := json.Unmarshal(initMsg, &payload) + suite.Require().NoError(err) + + store.Set(host.ClientStateKey(), clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), payload.ClientState)) + store.Set(host.ConsensusStateKey(payload.ClientState.LatestHeight), clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), payload.ConsensusState)) + + resp, err := json.Marshal(types.EmptyResult{}) + suite.Require().NoError(err) + + return &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) ([]byte, uint64, error) { + resp, err := json.Marshal(types.StatusResult{Status: exported.Active.String()}) + suite.Require().NoError(err) + return resp, wasmtesting.DefaultGasUsed, nil + }) + + db := dbm.NewMemDB() + app := simapp.NewSimApp(log.NewNopLogger(), db, nil, true, simtestutil.EmptyAppOptions{}, suite.mockVM) + + // reset DefaultTestingAppInit to its original value + ibctesting.DefaultTestingAppInit = setupTestingApp + return app, app.DefaultGenesis() +} + +// storeWasmCode stores the wasm code on chain and returns the checksum. +func storeWasmCode(suite *TypesTestSuite, wasmCode []byte) types.Checksum { + 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 +} diff --git a/modules/light-clients/08-wasm/types/validation.go b/modules/light-clients/08-wasm/types/validation.go new file mode 100644 index 00000000000..843d560ca46 --- /dev/null +++ b/modules/light-clients/08-wasm/types/validation.go @@ -0,0 +1,58 @@ +package types + +import ( + "strings" + + errorsmod "cosmossdk.io/errors" + + clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + host "github.com/cosmos/ibc-go/v8/modules/core/24-host" + "github.com/cosmos/ibc-go/v8/modules/core/exported" +) + +const maxWasmSize = 3 * 1024 * 1024 + +// ValidateWasmCode valides that the size of the wasm code is in the allowed range +// and that the contents are of a wasm binary. +func ValidateWasmCode(code []byte) error { + if len(code) == 0 { + return ErrWasmEmptyCode + } + if len(code) > maxWasmSize { + return ErrWasmCodeTooLarge + } + + return nil +} + +// MaxWasmByteSize returns the maximum allowed number of bytes for wasm bytecode +func MaxWasmByteSize() uint64 { + return maxWasmSize +} + +// ValidateWasmChecksum validates that the checksum is of the correct length +func ValidateWasmChecksum(checksum Checksum) error { + lenChecksum := len(checksum) + if lenChecksum == 0 { + return errorsmod.Wrap(ErrInvalidChecksum, "checksum cannot be empty") + } + if lenChecksum != 32 { // sha256 output is 256 bits long + return errorsmod.Wrapf(ErrInvalidChecksum, "expected length of 32 bytes, got %d", lenChecksum) + } + + return nil +} + +// ValidateClientID validates the client identifier by ensuring that it conforms +// to the 02-client identifier format and that it is a 08-wasm clientID. +func ValidateClientID(clientID string) error { + if !clienttypes.IsValidClientID(clientID) { + return errorsmod.Wrapf(host.ErrInvalidID, "invalid client identifier %s", clientID) + } + + if !strings.HasPrefix(clientID, exported.Wasm) { + return errorsmod.Wrapf(host.ErrInvalidID, "client identifier %s does not contain %s prefix", clientID, exported.Wasm) + } + + return nil +} diff --git a/modules/light-clients/08-wasm/types/validation_test.go b/modules/light-clients/08-wasm/types/validation_test.go new file mode 100644 index 00000000000..b8c42c7a9e0 --- /dev/null +++ b/modules/light-clients/08-wasm/types/validation_test.go @@ -0,0 +1,162 @@ +package types_test + +import ( + "crypto/rand" + "os" + "testing" + + "github.com/stretchr/testify/require" + + errorsmod "cosmossdk.io/errors" + + wasmtesting "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" + host "github.com/cosmos/ibc-go/v8/modules/core/24-host" + "github.com/cosmos/ibc-go/v8/modules/core/exported" + ibctesting "github.com/cosmos/ibc-go/v8/testing" +) + +func TestValidateWasmCode(t *testing.T) { + var code []byte + + testCases := []struct { + name string + malleate func() + expError error + }{ + { + "success", + func() { + code, _ = os.ReadFile("../test_data/ics10_grandpa_cw.wasm.gz") + }, + nil, + }, + { + "failure: empty byte slice", + func() { + code = []byte{} + }, + types.ErrWasmEmptyCode, + }, + { + "failure: byte slice too large", + func() { + expLength := types.MaxWasmByteSize() + 1 + code = make([]byte, expLength) + length, err := rand.Read(code) + require.NoError(t, err, t.Name()) + require.Equal(t, expLength, uint64(length), t.Name()) + }, + types.ErrWasmCodeTooLarge, + }, + } + + for _, tc := range testCases { + tc.malleate() + + err := types.ValidateWasmCode(code) + + if tc.expError == nil { + require.NoError(t, err, tc.name) + } else { + require.ErrorIs(t, err, tc.expError, tc.name) + } + } +} + +func TestValidateWasmChecksum(t *testing.T) { + var checksum types.Checksum + + testCases := []struct { + name string + malleate func() + expError error + }{ + { + "success", + func() { + hash, err := types.CreateChecksum(wasmtesting.Code) + require.NoError(t, err, t.Name()) + checksum = hash + }, + nil, + }, + { + "failure: nil byte slice", + func() { + checksum = nil + }, + errorsmod.Wrap(types.ErrInvalidChecksum, "checksum cannot be empty"), + }, + { + "failure: empty byte slice", + func() { + checksum = []byte{} + }, + errorsmod.Wrap(types.ErrInvalidChecksum, "checksum cannot be empty"), + }, + { + "failure: byte slice size is not 32", + func() { + checksum = []byte{1} + }, + errorsmod.Wrapf(types.ErrInvalidChecksum, "expected length of 32 bytes, got %d", 1), + }, + } + + for _, tc := range testCases { + tc.malleate() + + err := types.ValidateWasmChecksum(checksum) + + if tc.expError == nil { + require.NoError(t, err, tc.name) + } else { + require.ErrorContains(t, err, tc.expError.Error(), tc.name) + } + } +} + +func TestValidateClientID(t *testing.T) { + var clientID string + + testCases := []struct { + name string + malleate func() + expError error + }{ + { + "success: valid wasm client identifier", + func() { + clientID = defaultWasmClientID + }, + nil, + }, + { + "failure: empty clientID", + func() { + clientID = "" + }, + errorsmod.Wrapf(host.ErrInvalidID, "invalid client identifier %s", clientID), + }, + { + "failure: clientID is not a wasm client identifier", + func() { + clientID = ibctesting.FirstClientID + }, + errorsmod.Wrapf(host.ErrInvalidID, "client identifier %s does not contain %s prefix", ibctesting.FirstClientID, exported.Wasm), + }, + } + + for _, tc := range testCases { + tc.malleate() + + err := types.ValidateClientID(clientID) + + if tc.expError == nil { + require.NoError(t, err, tc.name) + } else { + require.ErrorContains(t, err, tc.expError.Error(), tc.name) + } + } +} diff --git a/modules/light-clients/08-wasm/types/vm.go b/modules/light-clients/08-wasm/types/vm.go new file mode 100644 index 00000000000..b23710139bd --- /dev/null +++ b/modules/light-clients/08-wasm/types/vm.go @@ -0,0 +1,313 @@ +package types + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "errors" + + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + + errorsmod "cosmossdk.io/errors" + storetypes "cosmossdk.io/store/types" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" + "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" + host "github.com/cosmos/ibc-go/v8/modules/core/24-host" + "github.com/cosmos/ibc-go/v8/modules/core/exported" +) + +var ( + VMGasRegister = NewDefaultWasmGasRegister() + // wasmvmAPI is a wasmvm.GoAPI implementation that is passed to the wasmvm, it + // doesn't implement any functionality, directly returning an error. + wasmvmAPI = wasmvm.GoAPI{ + HumanAddress: humanAddress, + CanonicalAddress: canonicalAddress, + } +) + +// instantiateContract calls vm.Instantiate with appropriate arguments. +func instantiateContract(ctx sdk.Context, clientStore storetypes.KVStore, checksum Checksum, msg []byte) (*wasmvmtypes.Response, error) { + sdkGasMeter := ctx.GasMeter() + multipliedGasMeter := NewMultipliedGasMeter(sdkGasMeter, VMGasRegister) + gasLimit := VMGasRegister.runtimeGasForContract(ctx) + + clientID, err := getClientID(clientStore) + if err != nil { + return nil, errorsmod.Wrap(err, "failed to retrieve clientID for wasm contract instantiation") + } + env := getEnv(ctx, clientID) + + msgInfo := wasmvmtypes.MessageInfo{ + Sender: "", + Funds: nil, + } + + ctx.GasMeter().ConsumeGas(VMGasRegister.NewContractInstanceCosts(true, len(msg)), "Loading CosmWasm module: instantiate") + response, gasUsed, err := ibcwasm.GetVM().Instantiate(checksum, env, msgInfo, msg, newStoreAdapter(clientStore), wasmvmAPI, nil, multipliedGasMeter, gasLimit, costJSONDeserialization) + VMGasRegister.consumeRuntimeGas(ctx, gasUsed) + return response, err +} + +// callContract calls vm.Sudo with internally constructed gas meter and environment. +func callContract(ctx sdk.Context, clientStore storetypes.KVStore, checksum Checksum, msg []byte) (*wasmvmtypes.Response, error) { + sdkGasMeter := ctx.GasMeter() + multipliedGasMeter := NewMultipliedGasMeter(sdkGasMeter, VMGasRegister) + gasLimit := VMGasRegister.runtimeGasForContract(ctx) + + clientID, err := getClientID(clientStore) + if err != nil { + return nil, errorsmod.Wrap(err, "failed to retrieve clientID for wasm contract call") + } + env := getEnv(ctx, clientID) + + ctx.GasMeter().ConsumeGas(VMGasRegister.InstantiateContractCosts(true, len(msg)), "Loading CosmWasm module: sudo") + resp, gasUsed, err := ibcwasm.GetVM().Sudo(checksum, env, msg, newStoreAdapter(clientStore), wasmvmAPI, nil, multipliedGasMeter, gasLimit, costJSONDeserialization) + VMGasRegister.consumeRuntimeGas(ctx, gasUsed) + return resp, err +} + +// migrateContract calls vm.Migrate with internally constructed gas meter and environment. +func migrateContract(ctx sdk.Context, clientID string, clientStore storetypes.KVStore, checksum Checksum, msg []byte) (*wasmvmtypes.Response, error) { + sdkGasMeter := ctx.GasMeter() + multipliedGasMeter := NewMultipliedGasMeter(sdkGasMeter, VMGasRegister) + gasLimit := VMGasRegister.runtimeGasForContract(ctx) + + env := getEnv(ctx, clientID) + + ctx.GasMeter().ConsumeGas(VMGasRegister.InstantiateContractCosts(true, len(msg)), "Loading CosmWasm module: migrate") + resp, gasUsed, err := ibcwasm.GetVM().Migrate(checksum, env, msg, newStoreAdapter(clientStore), wasmvmAPI, nil, multipliedGasMeter, gasLimit, costJSONDeserialization) + VMGasRegister.consumeRuntimeGas(ctx, gasUsed) + return resp, err +} + +// queryContract calls vm.Query. +func queryContract(ctx sdk.Context, clientStore storetypes.KVStore, checksum Checksum, msg []byte) ([]byte, error) { + sdkGasMeter := ctx.GasMeter() + multipliedGasMeter := NewMultipliedGasMeter(sdkGasMeter, VMGasRegister) + gasLimit := VMGasRegister.runtimeGasForContract(ctx) + + clientID, err := getClientID(clientStore) + if err != nil { + return nil, errorsmod.Wrap(err, "failed to retrieve clientID for wasm contract query") + } + env := getEnv(ctx, clientID) + + ctx.GasMeter().ConsumeGas(VMGasRegister.InstantiateContractCosts(true, len(msg)), "Loading CosmWasm module: query") + resp, gasUsed, err := ibcwasm.GetVM().Query(checksum, env, msg, newStoreAdapter(clientStore), wasmvmAPI, nil, multipliedGasMeter, gasLimit, costJSONDeserialization) + VMGasRegister.consumeRuntimeGas(ctx, gasUsed) + return resp, err +} + +// wasmInstantiate accepts a message to instantiate a wasm contract, JSON encodes it and calls instantiateContract. +func wasmInstantiate(ctx sdk.Context, cdc codec.BinaryCodec, clientStore storetypes.KVStore, cs *ClientState, payload InstantiateMessage) error { + encodedData, err := json.Marshal(payload) + if err != nil { + return errorsmod.Wrap(err, "failed to marshal payload for wasm contract instantiation") + } + + checksum := cs.Checksum + resp, err := instantiateContract(ctx, clientStore, checksum, encodedData) + if err != nil { + return errorsmod.Wrap(ErrWasmContractCallFailed, err.Error()) + } + + if err = checkResponse(resp); err != nil { + return errorsmod.Wrapf(err, "checksum (%s)", hex.EncodeToString(cs.Checksum)) + } + + newClientState, err := validatePostExecutionClientState(clientStore, cdc) + if err != nil { + return err + } + + // Checksum should only be able to be modified during migration. + if !bytes.Equal(checksum, newClientState.Checksum) { + return errorsmod.Wrapf(ErrWasmInvalidContractModification, "expected checksum %s, got %s", hex.EncodeToString(checksum), hex.EncodeToString(newClientState.Checksum)) + } + + return nil +} + +// wasmSudo calls the contract with the given payload and returns the result. +// wasmSudo returns an error if: +// - the payload cannot be marshaled to JSON +// - the contract call returns an error +// - the response of the contract call contains non-empty messages +// - the response of the contract call contains non-empty events +// - the response of the contract call contains non-empty attributes +// - the data bytes of the response cannot be unmarshaled into the result type +func wasmSudo[T ContractResult](ctx sdk.Context, cdc codec.BinaryCodec, clientStore storetypes.KVStore, cs *ClientState, payload SudoMsg) (T, error) { + var result T + + encodedData, err := json.Marshal(payload) + if err != nil { + return result, errorsmod.Wrap(err, "failed to marshal payload for wasm execution") + } + + checksum := cs.Checksum + resp, err := callContract(ctx, clientStore, checksum, encodedData) + if err != nil { + return result, errorsmod.Wrap(ErrWasmContractCallFailed, err.Error()) + } + + if err = checkResponse(resp); err != nil { + return result, errorsmod.Wrapf(err, "checksum (%s)", hex.EncodeToString(cs.Checksum)) + } + + if err := json.Unmarshal(resp.Data, &result); err != nil { + return result, errorsmod.Wrap(ErrWasmInvalidResponseData, err.Error()) + } + + newClientState, err := validatePostExecutionClientState(clientStore, cdc) + if err != nil { + return result, err + } + + // Checksum should only be able to be modified during migration. + if !bytes.Equal(checksum, newClientState.Checksum) { + return result, errorsmod.Wrapf(ErrWasmInvalidContractModification, "expected checksum %s, got %s", hex.EncodeToString(checksum), hex.EncodeToString(newClientState.Checksum)) + } + + return result, nil +} + +// validatePostExecutionClientState validates that the contract has not many any invalid modifications +// to the client state during execution. It ensures that +// - the client state is still present +// - the client state can be unmarshaled successfully. +// - the client state is of type *ClientState +func validatePostExecutionClientState(clientStore storetypes.KVStore, cdc codec.BinaryCodec) (*ClientState, error) { + key := host.ClientStateKey() + _, ok := clientStore.(migrateClientWrappedStore) + if ok { + key = append(subjectPrefix, key...) + } + + bz := clientStore.Get(key) + if len(bz) == 0 { + return nil, errorsmod.Wrap(ErrWasmInvalidContractModification, types.ErrClientNotFound.Error()) + } + + clientState, err := unmarshalClientState(cdc, bz) + if err != nil { + return nil, errorsmod.Wrap(ErrWasmInvalidContractModification, err.Error()) + } + + cs, ok := clientState.(*ClientState) + if !ok { + return nil, errorsmod.Wrapf(ErrWasmInvalidContractModification, "expected client state type %T, got %T", (*ClientState)(nil), clientState) + } + + return cs, nil +} + +// unmarshalClientState unmarshals the client state from the given bytes. +func unmarshalClientState(cdc codec.BinaryCodec, bz []byte) (exported.ClientState, error) { + var clientState exported.ClientState + if err := cdc.UnmarshalInterface(bz, &clientState); err != nil { + return nil, err + } + + return clientState, nil +} + +// wasmMigrate migrate calls the migrate entry point of the contract with the given payload and returns the result. +// wasmMigrate returns an error if: +// - the contract migration returns an error +func wasmMigrate(ctx sdk.Context, cdc codec.BinaryCodec, clientStore storetypes.KVStore, cs *ClientState, clientID string, payload []byte) error { + resp, err := migrateContract(ctx, clientID, clientStore, cs.Checksum, payload) + if err != nil { + return errorsmod.Wrapf(ErrWasmContractCallFailed, err.Error()) + } + + if err = checkResponse(resp); err != nil { + return errorsmod.Wrapf(err, "checksum (%s)", hex.EncodeToString(cs.Checksum)) + } + + _, err = validatePostExecutionClientState(clientStore, cdc) + return err +} + +// wasmQuery queries the contract with the given payload and returns the result. +// wasmQuery returns an error if: +// - the payload cannot be marshaled to JSON +// - the contract query returns an error +// - the data bytes of the response cannot be unmarshal into the result type +func wasmQuery[T ContractResult](ctx sdk.Context, clientStore storetypes.KVStore, cs *ClientState, payload QueryMsg) (T, error) { + var result T + + encodedData, err := json.Marshal(payload) + if err != nil { + return result, errorsmod.Wrap(err, "failed to marshal payload for wasm query") + } + + resp, err := queryContract(ctx, clientStore, cs.Checksum, encodedData) + if err != nil { + return result, errorsmod.Wrap(ErrWasmContractCallFailed, err.Error()) + } + + if err := json.Unmarshal(resp, &result); err != nil { + return result, errorsmod.Wrapf(ErrWasmInvalidResponseData, "failed to unmarshal result of wasm query: %v", err) + } + + return result, nil +} + +// getEnv returns the state of the blockchain environment the contract is running on +func getEnv(ctx sdk.Context, contractAddr string) wasmvmtypes.Env { + chainID := ctx.BlockHeader().ChainID + height := ctx.BlockHeader().Height + + // safety checks before casting below + if height < 0 { + panic(errors.New("block height must never be negative")) + } + nsec := ctx.BlockTime().UnixNano() + if nsec < 0 { + panic(errors.New("block (unix) time must never be negative ")) + } + + env := wasmvmtypes.Env{ + Block: wasmvmtypes.BlockInfo{ + Height: uint64(height), + Time: uint64(nsec), + ChainID: chainID, + }, + Contract: wasmvmtypes.ContractInfo{ + Address: contractAddr, + }, + } + + return env +} + +func humanAddress(canon []byte) (string, uint64, error) { + return "", 0, errors.New("humanAddress not implemented") +} + +func canonicalAddress(human string) ([]byte, uint64, error) { + return nil, 0, errors.New("canonicalAddress not implemented") +} + +// checkResponse returns an error if the response from a sudo, instantiate or migrate call +// to the Wasm VM contains messages, events or attributes. +func checkResponse(response *wasmvmtypes.Response) error { + // Only allow Data to flow back to us. SubMessages, Events and Attributes are not allowed. + if len(response.Messages) > 0 { + return ErrWasmSubMessagesNotAllowed + } + if len(response.Events) > 0 { + return ErrWasmEventsNotAllowed + } + if len(response.Attributes) > 0 { + return ErrWasmAttributesNotAllowed + } + + return nil +} diff --git a/modules/light-clients/08-wasm/types/wasm.go b/modules/light-clients/08-wasm/types/wasm.go new file mode 100644 index 00000000000..cfec1c03b2b --- /dev/null +++ b/modules/light-clients/08-wasm/types/wasm.go @@ -0,0 +1,54 @@ +package types + +import ( + "context" + + wasmvm "github.com/CosmWasm/wasmvm" + wasmvmtypes "github.com/CosmWasm/wasmvm/types" + + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" +) + +// Checksum is a type alias used for wasm byte code checksums. +type Checksum = wasmvmtypes.Checksum + +// CreateChecksum creates a sha256 checksum from the given wasm code, it forwards the +// call to the wasmvm package. The code is checked for the following conditions: +// - code length is zero. +// - code length is less than 4 bytes (magic number length). +// - code does not start with the wasm magic number. +func CreateChecksum(code []byte) (Checksum, error) { + return wasmvm.CreateChecksum(code) +} + +// GetAllChecksums is a helper to get all checksums from the store. +// It returns an empty slice if no checksums are found +func GetAllChecksums(ctx context.Context) ([]Checksum, error) { + iterator, err := ibcwasm.Checksums.Iterate(ctx, nil) + if err != nil { + return nil, err + } + + keys, err := iterator.Keys() + if err != nil { + return nil, err + } + + checksums := []Checksum{} + for _, key := range keys { + checksums = append(checksums, key) + } + + return checksums, nil +} + +// HasChecksum returns true if the given checksum exists in the store and +// false otherwise. +func HasChecksum(ctx context.Context, checksum Checksum) bool { + found, err := ibcwasm.Checksums.Has(ctx, checksum) + if err != nil { + return false + } + + return found +} diff --git a/modules/light-clients/08-wasm/types/wasm_test.go b/modules/light-clients/08-wasm/types/wasm_test.go new file mode 100644 index 00000000000..65b9b6574c5 --- /dev/null +++ b/modules/light-clients/08-wasm/types/wasm_test.go @@ -0,0 +1,124 @@ +package types_test + +import ( + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" + wasmtesting "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing" + "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" +) + +func (suite *TypesTestSuite) 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() + }, + 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() + + err := ibcwasm.Checksums.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 := types.GetAllChecksums(suite.chainA.GetContext()) + suite.Require().NoError(err) + tc.expResult(checksums) + }) + } +} + +func (suite *TypesTestSuite) TestAddChecksum() { + suite.SetupWasmWithMockVM() + + checksums, err := types.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 = ibcwasm.Checksums.Set(suite.chainA.GetContext(), checksum1) + suite.Require().NoError(err) + err = ibcwasm.Checksums.Set(suite.chainA.GetContext(), checksum2) + suite.Require().NoError(err) + + // Test adding the same checksum twice + err = ibcwasm.Checksums.Set(suite.chainA.GetContext(), checksum1) + suite.Require().NoError(err) + + checksums, err = types.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 *TypesTestSuite) TestHasChecksum() { + var checksum types.Checksum + + testCases := []struct { + name string + malleate func() + exprResult bool + }{ + { + "success: checksum exists", + func() { + checksum = types.Checksum("checksum") + err := ibcwasm.Checksums.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 := types.HasChecksum(suite.chainA.GetContext(), checksum) + suite.Require().Equal(tc.exprResult, result) + }) + } +} From e83173053de021e4efa602ad3375493679a41821 Mon Sep 17 00:00:00 2001 From: Damian Nolan Date: Wed, 29 Nov 2023 17:43:05 +0100 Subject: [PATCH 2/2] lint: simplify newHash[:] -> newHash to satifsy linting overlords --- modules/light-clients/08-wasm/types/migrate_contract_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/light-clients/08-wasm/types/migrate_contract_test.go b/modules/light-clients/08-wasm/types/migrate_contract_test.go index e225ab5bbea..59a6a4dfdbf 100644 --- a/modules/light-clients/08-wasm/types/migrate_contract_test.go +++ b/modules/light-clients/08-wasm/types/migrate_contract_test.go @@ -34,7 +34,7 @@ func (suite *TypesTestSuite) TestMigrateContract() { suite.Require().NoError(err) payload = []byte{1} - expChecksum := wasmvmtypes.ForceNewChecksum(hex.EncodeToString(newHash[:])) + expChecksum := wasmvmtypes.ForceNewChecksum(hex.EncodeToString(newHash)) suite.mockVM.MigrateFn = func(checksum wasmvm.Checksum, env wasmvmtypes.Env, msg []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.Response, uint64, error) { suite.Require().Equal(expChecksum, checksum)