From 1ba646e3a77de99c4921d339ef9fd972d6381565 Mon Sep 17 00:00:00 2001 From: DimitrisJim Date: Fri, 22 Mar 2024 14:48:34 +0200 Subject: [PATCH 1/6] tests: move tests from client state to light client module for 08-wasm. --- .../08-wasm/light_client_module_test.go | 1542 +++++++++++++++-- .../08-wasm/types/client_state_test.go | 597 ------- .../08-wasm/types/misbehaviour_handle_test.go | 102 -- .../08-wasm/types/proposal_handle_test.go | 141 -- .../08-wasm/types/update_test.go | 309 ---- .../08-wasm/types/upgrade_test.go | 276 --- 6 files changed, 1404 insertions(+), 1563 deletions(-) delete mode 100644 modules/light-clients/08-wasm/types/misbehaviour_handle_test.go delete mode 100644 modules/light-clients/08-wasm/types/proposal_handle_test.go delete mode 100644 modules/light-clients/08-wasm/types/update_test.go delete mode 100644 modules/light-clients/08-wasm/types/upgrade_test.go diff --git a/modules/light-clients/08-wasm/light_client_module_test.go b/modules/light-clients/08-wasm/light_client_module_test.go index a943f7840b7..7a7ecddae55 100644 --- a/modules/light-clients/08-wasm/light_client_module_test.go +++ b/modules/light-clients/08-wasm/light_client_module_test.go @@ -2,13 +2,16 @@ package wasm_test import ( "encoding/json" + "errors" + "fmt" "time" wasmvm "github.com/CosmWasm/wasmvm/v2" wasmvmtypes "github.com/CosmWasm/wasmvm/v2/types" - codectypes "github.com/cosmos/cosmos-sdk/codec/types" + errorsmod "cosmossdk.io/errors" + "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" @@ -16,58 +19,164 @@ import ( 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" ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint" ibctesting "github.com/cosmos/ibc-go/v8/testing" + ibcmock "github.com/cosmos/ibc-go/v8/testing/mock" ) const ( tmClientID = "07-tendermint-0" - wasmClientID = "08-wasm-100" + wasmClientID = "08-wasm-0" + // Used for checks where look ups for valid client id should fail. + unusedWasmClientID = "08-wasm-100" ) -func (suite *WasmTestSuite) TestRecoverClient() { +func (suite *WasmTestSuite) TestStatus() { + var clientID string + + 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) (*wasmvmtypes.QueryResult, uint64, error) { + resp, err := json.Marshal(types.StatusResult{Status: exported.Frozen.String()}) + suite.Require().NoError(err) + return &wasmvmtypes.QueryResult{Ok: 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) (*wasmvmtypes.QueryResult, uint64, error) { + resp, err := json.Marshal(types.StatusResult{Status: exported.Expired.String()}) + suite.Require().NoError(err) + return &wasmvmtypes.QueryResult{Ok: 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) (*wasmvmtypes.QueryResult, 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, + }, + { + "failure: cannot find client state", + func() { + clientID = unusedWasmClientID + }, + exported.Unknown, + }, + } + + 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) + clientID = endpoint.ClientID + + lightClientModule, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(clientID) + suite.Require().True(found) + + tc.malleate() + + status := lightClientModule.Status(suite.chainA.GetContext(), clientID) + suite.Require().Equal(tc.expStatus, status) + }) + } +} + +func (suite *WasmTestSuite) TestTimestampAtHeight() { var ( - expectedClientStateBz []byte - subjectClientID, substituteClientID string + clientID string + height exported.Height ) + expectedTimestamp := uint64(time.Now().UnixNano()) testCases := []struct { name string malleate func() expErr error }{ - // TODO(02-client routing): add successful test when light client module does not call into 08-wasm ClientState - // { - // "success", - // func() { - // }, - // nil, - // }, { - "cannot parse malformed substitute client ID", + "success", func() { - substituteClientID = ibctesting.InvalidID + suite.mockVM.RegisterQueryCallback(types.TimestampAtHeightMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, queryMsg []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, 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.VerifyClientMessage) + + resp, err := json.Marshal(types.TimestampAtHeightResult{Timestamp: expectedTimestamp}) + suite.Require().NoError(err) + + return &wasmvmtypes.QueryResult{Ok: resp}, wasmtesting.DefaultGasUsed, nil + }) }, - host.ErrInvalidID, + nil, }, { - "substitute client ID does not contain 08-wasm prefix", + "failure: vm returns error", func() { - substituteClientID = tmClientID + suite.mockVM.RegisterQueryCallback(types.TimestampAtHeightMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { + return nil, 0, wasmtesting.ErrMockVM + }) }, - clienttypes.ErrInvalidClientType, + types.ErrVMError, }, { - "cannot find subject client state", + "failure: contract returns error", func() { - subjectClientID = wasmClientID + suite.mockVM.RegisterQueryCallback(types.TimestampAtHeightMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { + return &wasmvmtypes.QueryResult{Err: wasmtesting.ErrMockContract.Error()}, 0, nil + }) }, - clienttypes.ErrClientNotFound, + types.ErrWasmContractCallFailed, + }, + { + "failure: error: invalid height", + func() { + height = ibcmock.Height{} + }, + ibcerrors.ErrInvalidType, }, { - "cannot find substitute client state", + "failure: cannot find client state", func() { - substituteClientID = wasmClientID + clientID = unusedWasmClientID }, clienttypes.ErrClientNotFound, }, @@ -77,209 +186,1366 @@ func (suite *WasmTestSuite) TestRecoverClient() { tc := tc suite.Run(tc.name, func() { suite.SetupWasmWithMockVM() - expectedClientStateBz = nil - subjectEndpoint := wasmtesting.NewWasmEndpoint(suite.chainA) - err := subjectEndpoint.CreateClient() + endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) + err := endpoint.CreateClient() suite.Require().NoError(err) - subjectClientID = subjectEndpoint.ClientID + clientID = endpoint.ClientID - subjectClientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), subjectClientID) + clientState, ok := endpoint.GetClientState().(*types.ClientState) + suite.Require().True(ok) - substituteEndpoint := wasmtesting.NewWasmEndpoint(suite.chainA) - err = substituteEndpoint.CreateClient() - suite.Require().NoError(err) - substituteClientID = substituteEndpoint.ClientID + height = clientState.LatestHeight - lightClientModule, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(subjectClientID) + lightClientModule, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(clientID) suite.Require().True(found) tc.malleate() - err = lightClientModule.RecoverClient(suite.chainA.GetContext(), subjectClientID, substituteClientID) + timestamp, err := lightClientModule.TimestampAtHeight(suite.chainA.GetContext(), clientID, height) expPass := tc.expErr == nil if expPass { suite.Require().NoError(err) - - clientStateBz := subjectClientStore.Get(host.ClientStateKey()) - suite.Require().Equal(expectedClientStateBz, clientStateBz) + suite.Require().Equal(expectedTimestamp, timestamp) } else { suite.Require().ErrorIs(err, tc.expErr) + suite.Require().Equal(uint64(0), timestamp) } }) } } -func (suite *WasmTestSuite) TestVerifyUpgradeAndUpdateState() { +func (suite *WasmTestSuite) TestInitialize() { var ( - clientID string - clientState *types.ClientState - upgradedClientState exported.ClientState - upgradedConsensusState exported.ConsensusState - upgradedClientStateAny, upgradedConsensusStateAny *codectypes.Any - upgradedClientStateProof, upgradedConsensusStateProof []byte + consensusState exported.ConsensusState + clientState exported.ClientState ) testCases := []struct { name string malleate func() - expErr error + expError error }{ { - "success", + "success: new mock client", + func() {}, + nil, + }, + { + "success: validate contract address", func() { - suite.mockVM.RegisterSudoCallback(types.VerifyUpgradeAndUpdateStateMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { - var payload types.SudoMsg + 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.ContractResult, uint64, error) { + var payload types.InstantiateMessage + err := json.Unmarshal(initMsg, &payload) + suite.Require().NoError(err) - err := json.Unmarshal(sudoMsg, &payload) + suite.Require().Equal(env.Contract.Address, wasmClientID) + + wrappedClientState := clienttypes.MustUnmarshalClientState(suite.chainA.App.AppCodec(), payload.ClientState).(*ibctm.ClientState) + + clientState := types.NewClientState(payload.ClientState, payload.Checksum, wrappedClientState.LatestHeight) + clientStateBz := clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), clientState) + store.Set(host.ClientStateKey(), clientStateBz) + + consensusState := types.NewConsensusState(payload.ConsensusState) + consensusStateBz := clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), consensusState) + store.Set(host.ConsensusStateKey(clientState.LatestHeight), consensusStateBz) + + resp, err := json.Marshal(types.EmptyResult{}) suite.Require().NoError(err) - expectedUpgradedClient, ok := upgradedClientState.(*types.ClientState) - suite.Require().True(ok) - expectedUpgradedConsensus, ok := upgradedConsensusState.(*types.ConsensusState) - suite.Require().True(ok) + return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: resp}}, 0, nil + } + }, + nil, + }, + { + "failure: cannot unmarshal client state", + func() { + clientState = &solomachine.ClientState{Sequence: 20} + }, + fmt.Errorf("proto: wrong wireType = 0 for field Data"), + }, + { + "failure: client state is invalid", + func() { + clientState = &types.ClientState{} + }, + types.ErrInvalidData, + }, + { + "failure: invalid consensus state", + func() { + // set upgraded consensus state to solomachine consensus state + consensusState = &solomachine.ConsensusState{} + }, + types.ErrInvalidData, + }, + { + "failure: checksum has not been stored.", + func() { + clientState = types.NewClientState([]byte{1}, []byte("unknown"), clienttypes.NewHeight(0, 1)) + }, + types.ErrInvalidChecksum, + }, + { + "failure: vm 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.ContractResult, uint64, error) { + return nil, 0, wasmtesting.ErrMockVM + } + }, + types.ErrVMError, + }, + { + "failure: contract 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.ContractResult, uint64, error) { + return &wasmvmtypes.ContractResult{Err: wasmtesting.ErrMockContract.Error()}, 0, nil + } + }, + types.ErrWasmContractCallFailed, + }, + } - // verify payload values - suite.Require().Equal(expectedUpgradedClient.Data, payload.VerifyUpgradeAndUpdateState.UpgradeClientState) - suite.Require().Equal(expectedUpgradedConsensus.Data, payload.VerifyUpgradeAndUpdateState.UpgradeConsensusState) - suite.Require().Equal(upgradedClientStateProof, payload.VerifyUpgradeAndUpdateState.ProofUpgradeClient) - suite.Require().Equal(upgradedConsensusStateProof, payload.VerifyUpgradeAndUpdateState.ProofUpgradeConsensusState) + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() - // verify other Sudo fields are nil - suite.Require().Nil(payload.UpdateState) - suite.Require().Nil(payload.UpdateStateOnMisbehaviour) - suite.Require().Nil(payload.VerifyMembership) - suite.Require().Nil(payload.VerifyNonMembership) + wrappedClientStateBz := clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), wasmtesting.MockTendermitClientState) + wrappedClientConsensusStateBz := clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), wasmtesting.MockTendermintClientConsensusState) + clientState = types.NewClientState(wrappedClientStateBz, suite.checksum, wasmtesting.MockTendermitClientState.LatestHeight) + consensusState = types.NewConsensusState(wrappedClientConsensusStateBz) - data, err := json.Marshal(types.EmptyResult{}) + clientID := suite.chainA.App.GetIBCKeeper().ClientKeeper.GenerateClientIdentifier(suite.chainA.GetContext(), clientState.ClientType()) + + lightClientModule, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(clientID) + suite.Require().True(found) + + tc.malleate() + + // Marshal client state and consensus state: + clientStateBz := suite.chainA.Codec.MustMarshal(clientState) + consensusStateBz := suite.chainA.Codec.MustMarshal(consensusState) + + err := lightClientModule.Initialize(suite.chainA.GetContext(), clientID, clientStateBz, consensusStateBz) + + expPass := tc.expError == nil + if expPass { + suite.Require().NoError(err) + } else { + suite.Require().ErrorContains(err, tc.expError.Error()) + } + }) + } +} + +func (suite *WasmTestSuite) TestVerifyMembership() { + var ( + clientState *types.ClientState + expClientStateBz []byte + path exported.Path + proof []byte + proofHeight exported.Height + value []byte + clientID string + ) + + 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.ContractResult, uint64, error) { + var payload types.SudoMsg + err := json.Unmarshal(sudoMsg, &payload) suite.Require().NoError(err) - // set new client state and consensus state - bz, err := suite.chainA.Codec.MarshalInterface(upgradedClientState) + bz, err := json.Marshal(types.EmptyResult{}) + suite.Require().NoError(err) + + return &wasmvmtypes.ContractResult{Ok: &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.ContractResult, uint64, error) { + var payload types.SudoMsg + err := json.Unmarshal(sudoMsg, &payload) suite.Require().NoError(err) - store.Set(host.ClientStateKey(), bz) + 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 = suite.chainA.Codec.MarshalInterface(upgradedConsensusState) + bz, err := json.Marshal(types.EmptyResult{}) suite.Require().NoError(err) - store.Set(host.ConsensusStateKey(expectedUpgradedClient.LatestHeight), bz) + expClientStateBz = wasmtesting.CreateMockClientStateBz(suite.chainA.Codec, suite.checksum) + store.Set(host.ClientStateKey(), expClientStateBz) - return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: data}}, wasmtesting.DefaultGasUsed, nil + return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: bz}}, wasmtesting.DefaultGasUsed, nil }) }, nil, }, { - "cannot find client state", + "failure: cannot find client state", func() { - clientID = wasmClientID + clientID = unusedWasmClientID }, clienttypes.ErrClientNotFound, }, { - "upgraded client state is not wasm client state", + "failure: contract returns invalid proof error", func() { - upgradedClientStateAny = &codectypes.Any{ - Value: []byte("invalid client state bytes"), - } + 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.ContractResult, uint64, error) { + return &wasmvmtypes.ContractResult{Err: commitmenttypes.ErrInvalidProof.Error()}, wasmtesting.DefaultGasUsed, nil + }) }, - clienttypes.ErrInvalidClient, + types.ErrWasmContractCallFailed, }, { - "upgraded consensus state is not wasm consensus sate", + "failure: proof height greater than client state latest height", func() { - upgradedConsensusStateAny = &codectypes.Any{ - Value: []byte("invalid consensus state bytes"), - } + proofHeight = clienttypes.NewHeight(1, 100) }, - clienttypes.ErrInvalidConsensus, + ibcerrors.ErrInvalidHeight, }, { - "upgraded client state height is not greater than current height", + "failure: invalid path argument", func() { - var err error - latestHeight := clientState.LatestHeight - newLatestHeight := clienttypes.NewHeight(latestHeight.GetRevisionNumber(), latestHeight.GetRevisionHeight()-1) - - wrappedUpgradedClient := wasmtesting.CreateMockTendermintClientState(newLatestHeight) - wrappedUpgradedClientBz := clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), wrappedUpgradedClient) - upgradedClientState = types.NewClientState(wrappedUpgradedClientBz, clientState.Checksum, newLatestHeight) - upgradedClientStateAny, err = codectypes.NewAnyWithValue(upgradedClientState) - suite.Require().NoError(err) + path = ibcmock.KeyPath{} }, - ibcerrors.ErrInvalidHeight, + ibcerrors.ErrInvalidType, + }, + { + "failure: proof height is invalid type", + func() { + proofHeight = ibcmock.Height{} + }, + ibcerrors.ErrInvalidType, }, } for _, tc := range testCases { - tc := tc suite.Run(tc.name, func() { - suite.SetupWasmWithMockVM() // reset suite + var ok bool + suite.SetupWasmWithMockVM() endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) err := endpoint.CreateClient() suite.Require().NoError(err) clientID = endpoint.ClientID - clientState = endpoint.GetClientState().(*types.ClientState) - latestHeight := clientState.LatestHeight - - newLatestHeight := clienttypes.NewHeight(latestHeight.GetRevisionNumber(), latestHeight.GetRevisionHeight()+1) - wrappedUpgradedClient := wasmtesting.CreateMockTendermintClientState(newLatestHeight) - wrappedUpgradedClientBz := clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), wrappedUpgradedClient) - upgradedClientState = types.NewClientState(wrappedUpgradedClientBz, clientState.Checksum, newLatestHeight) - upgradedClientStateAny, err = codectypes.NewAnyWithValue(upgradedClientState) - suite.Require().NoError(err) + path = commitmenttypes.NewMerklePath("/ibc/key/path") + proof = wasmtesting.MockValidProofBz + proofHeight = clienttypes.NewHeight(0, 1) + value = []byte("value") - wrappedUpgradedConsensus := ibctm.NewConsensusState(time.Now(), commitmenttypes.NewMerkleRoot([]byte("new-hash")), []byte("new-nextValsHash")) - wrappedUpgradedConsensusBz := clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), wrappedUpgradedConsensus) - upgradedConsensusState = types.NewConsensusState(wrappedUpgradedConsensusBz) - upgradedConsensusStateAny, err = codectypes.NewAnyWithValue(upgradedConsensusState) - suite.Require().NoError(err) + clientState, ok = endpoint.GetClientState().(*types.ClientState) + suite.Require().True(ok) lightClientModule, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(clientID) suite.Require().True(found) tc.malleate() - clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), clientID) - - upgradedClientStateProof = wasmtesting.MockUpgradedClientStateProofBz - upgradedConsensusStateProof = wasmtesting.MockUpgradedConsensusStateProofBz - - err = lightClientModule.VerifyUpgradeAndUpdateState( - suite.chainA.GetContext(), - clientID, - upgradedClientStateAny.Value, - upgradedConsensusStateAny.Value, - upgradedClientStateProof, - upgradedConsensusStateProof, - ) + err = lightClientModule.VerifyMembership(suite.chainA.GetContext(), clientID, proofHeight, 0, 0, proof, path, value) - expPass := tc.expErr == nil + expPass := tc.expError == nil if expPass { + clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), clientID) + suite.Require().NoError(err) + suite.Require().Equal(expClientStateBz, clientStore.Get(host.ClientStateKey())) + } else { + suite.Require().ErrorIs(err, tc.expError) + } + }) + } +} - // verify new client state and consensus state - clientStateBz := clientStore.Get(host.ClientStateKey()) - suite.Require().NotEmpty(clientStateBz) +func (suite *WasmTestSuite) TestVerifyNonMembership() { + var ( + clientState *types.ClientState + expClientStateBz []byte + path exported.Path + proof []byte + proofHeight exported.Height + clientID string + ) - expClientStateBz, err := suite.chainA.Codec.MarshalInterface(upgradedClientState) - suite.Require().NoError(err) - suite.Require().Equal(expClientStateBz, clientStateBz) + 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.ContractResult, uint64, error) { + var payload types.SudoMsg + err := json.Unmarshal(sudoMsg, &payload) + suite.Require().NoError(err) - consensusStateBz := clientStore.Get(host.ConsensusStateKey(endpoint.GetClientLatestHeight())) - suite.Require().NotEmpty(consensusStateBz) + 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) - expConsensusStateBz, err := suite.chainA.Codec.MarshalInterface(upgradedConsensusState) - suite.Require().NoError(err) - suite.Require().Equal(expConsensusStateBz, consensusStateBz) - } else { - suite.Require().Error(err) - suite.Require().ErrorIs(err, tc.expErr) - } + bz, err := json.Marshal(types.EmptyResult{}) + suite.Require().NoError(err) + + return &wasmvmtypes.ContractResult{Ok: &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.ContractResult, 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.ContractResult{Ok: &wasmvmtypes.Response{Data: bz}}, wasmtesting.DefaultGasUsed, nil + }) + }, + nil, + }, + { + "failure: cannot find client state", + func() { + clientID = unusedWasmClientID + }, + clienttypes.ErrClientNotFound, + }, + { + "failure: wasm vm returns 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.ContractResult, uint64, error) { + return nil, wasmtesting.DefaultGasUsed, wasmtesting.ErrMockVM + }) + }, + types.ErrVMError, + }, + { + "failure: contract 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.ContractResult, uint64, error) { + return &wasmvmtypes.ContractResult{Err: commitmenttypes.ErrInvalidProof.Error()}, wasmtesting.DefaultGasUsed, nil + }) + }, + types.ErrWasmContractCallFailed, + }, + { + "failure: proof height greater than client state latest height", + func() { + proofHeight = clienttypes.NewHeight(1, 100) + }, + ibcerrors.ErrInvalidHeight, + }, + { + "failure: invalid path argument", + func() { + path = ibcmock.KeyPath{} + }, + ibcerrors.ErrInvalidType, + }, + { + "failure: proof height is invalid type", + func() { + proofHeight = ibcmock.Height{} + }, + ibcerrors.ErrInvalidType, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + var ok bool + suite.SetupWasmWithMockVM() + + endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) + err := endpoint.CreateClient() + suite.Require().NoError(err) + clientID = endpoint.ClientID + + path = commitmenttypes.NewMerklePath("/ibc/key/path") + proof = wasmtesting.MockInvalidProofBz + proofHeight = clienttypes.NewHeight(0, 1) + + clientState, ok = endpoint.GetClientState().(*types.ClientState) + suite.Require().True(ok) + + lightClientModule, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(clientID) + suite.Require().True(found) + + tc.malleate() + + err = lightClientModule.VerifyNonMembership(suite.chainA.GetContext(), clientID, proofHeight, 0, 0, proof, path) + + expPass := tc.expError == nil + if expPass { + clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), clientID) + + suite.Require().NoError(err) + suite.Require().Equal(expClientStateBz, clientStore.Get(host.ClientStateKey())) + } else { + suite.Require().ErrorIs(err, tc.expError) + } + }) + } +} + +func (suite *WasmTestSuite) TestVerifyClientMessage() { + var ( + clientMsg exported.ClientMessage + clientID string + ) + + testCases := []struct { + name string + malleate func() + expErr error + }{ + { + "success: valid misbehaviour", + func() { + suite.mockVM.RegisterQueryCallback(types.VerifyClientMessageMsg{}, func(_ wasmvm.Checksum, env wasmvmtypes.Env, queryMsg []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { + var msg *types.QueryMsg + + err := json.Unmarshal(queryMsg, &msg) + suite.Require().NoError(err) + + suite.Require().NotNil(msg.VerifyClientMessage) + suite.Require().NotNil(msg.VerifyClientMessage.ClientMessage) + suite.Require().Nil(msg.Status) + suite.Require().Nil(msg.CheckForMisbehaviour) + suite.Require().Nil(msg.TimestampAtHeight) + + suite.Require().Equal(env.Contract.Address, wasmClientID) + + resp, err := json.Marshal(types.EmptyResult{}) + suite.Require().NoError(err) + + return &wasmvmtypes.QueryResult{Ok: resp}, wasmtesting.DefaultGasUsed, nil + }) + }, + nil, + }, + { + "failure: cannot find client state", + func() { + clientID = unusedWasmClientID + }, + clienttypes.ErrClientNotFound, + }, + { + "failure: invalid client message", + func() { + clientMsg = &ibctm.Header{} + + suite.mockVM.RegisterQueryCallback(types.VerifyClientMessageMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, queryMsg []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { + resp, err := json.Marshal(types.EmptyResult{}) + suite.Require().NoError(err) + + return &wasmvmtypes.QueryResult{Ok: resp}, wasmtesting.DefaultGasUsed, nil + }) + }, + ibcerrors.ErrInvalidType, + }, + { + "failure: error return from vm", + func() { + suite.mockVM.RegisterQueryCallback(types.VerifyClientMessageMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, queryMsg []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { + return nil, 0, wasmtesting.ErrMockVM + }) + }, + types.ErrVMError, + }, + { + "failure: error return from contract", + func() { + suite.mockVM.RegisterQueryCallback(types.VerifyClientMessageMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, queryMsg []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { + return &wasmvmtypes.QueryResult{Err: wasmtesting.ErrMockContract.Error()}, 0, nil + }) + }, + types.ErrWasmContractCallFailed, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + // reset suite to create fresh application state + suite.SetupWasmWithMockVM() + + endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) + err := endpoint.CreateClient() + suite.Require().NoError(err) + clientID = endpoint.ClientID + + clientMsg = &types.ClientMessage{ + Data: clienttypes.MustMarshalClientMessage(suite.chainA.App.AppCodec(), wasmtesting.MockTendermintClientHeader), + } + + lightClientModule, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(clientID) + suite.Require().True(found) + + tc.malleate() + + err = lightClientModule.VerifyClientMessage(suite.chainA.GetContext(), clientID, clientMsg) + + expPass := tc.expErr == nil + if expPass { + suite.Require().NoError(err) + } else { + suite.Require().ErrorIs(err, tc.expErr) + } + }) + } +} + +func (suite *WasmTestSuite) TestVerifyUpgradeAndUpdateState() { + var ( + upgradedClient exported.ClientState + upgradedConsState exported.ConsensusState + upgradedClientProof []byte + upgradedConsensusStateProof []byte + clientID string + ) + + testCases := []struct { + name string + malleate func() + expErr error + }{ + { + "success: successful upgrade", + func() { + suite.mockVM.RegisterSudoCallback(types.VerifyUpgradeAndUpdateStateMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + var payload types.SudoMsg + + err := json.Unmarshal(sudoMsg, &payload) + suite.Require().NoError(err) + + expectedUpgradedClient, ok := upgradedClient.(*types.ClientState) + suite.Require().True(ok) + expectedUpgradedConsensus, ok := upgradedConsState.(*types.ConsensusState) + suite.Require().True(ok) + + // verify payload values + suite.Require().Equal(expectedUpgradedClient.Data, payload.VerifyUpgradeAndUpdateState.UpgradeClientState) + suite.Require().Equal(expectedUpgradedConsensus.Data, payload.VerifyUpgradeAndUpdateState.UpgradeConsensusState) + suite.Require().Equal(upgradedClientProof, payload.VerifyUpgradeAndUpdateState.ProofUpgradeClient) + suite.Require().Equal(upgradedConsensusStateProof, payload.VerifyUpgradeAndUpdateState.ProofUpgradeConsensusState) + + // verify other Sudo fields are nil + suite.Require().Nil(payload.UpdateState) + suite.Require().Nil(payload.UpdateStateOnMisbehaviour) + suite.Require().Nil(payload.VerifyMembership) + suite.Require().Nil(payload.VerifyNonMembership) + + data, err := json.Marshal(types.EmptyResult{}) + suite.Require().NoError(err) + + // set new client state and consensus state + wrappedUpgradedClient := clienttypes.MustUnmarshalClientState(suite.chainA.App.AppCodec(), expectedUpgradedClient.Data).(*ibctm.ClientState) + store.Set(host.ClientStateKey(), clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), upgradedClient)) + store.Set(host.ConsensusStateKey(wrappedUpgradedClient.LatestHeight), clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), upgradedConsState)) + + return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: data}}, wasmtesting.DefaultGasUsed, nil + }) + }, + nil, + }, + { + "failure: invalid client state", + func() { + upgradedClient = &solomachine.ClientState{Sequence: 20} + }, + clienttypes.ErrInvalidClient, + }, + { + "failure: invalid height", + func() { + upgradedClient = &types.ClientState{LatestHeight: clienttypes.ZeroHeight()} + }, + ibcerrors.ErrInvalidHeight, + }, + { + "failure: cannot find client state", + func() { + clientID = unusedWasmClientID + }, + clienttypes.ErrClientNotFound, + }, + /* TODO(jim): Get back to these. + { + "failure: upgraded consensus state is not wasm consensus state", + func() { + // set upgraded consensus state to solomachine consensus state + upgradedConsState = &solomachine.ConsensusState{} + }, + clienttypes.ErrInvalidConsensus, + }, + */ + { + "failure: vm returns error", + func() { + suite.mockVM.RegisterSudoCallback(types.VerifyUpgradeAndUpdateStateMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + return nil, 0, wasmtesting.ErrMockVM + }) + }, + types.ErrVMError, + }, + { + "failure: contract returns error", + func() { + suite.mockVM.RegisterSudoCallback(types.VerifyUpgradeAndUpdateStateMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + return &wasmvmtypes.ContractResult{Err: wasmtesting.ErrMockContract.Error()}, 0, nil + }) + }, + types.ErrWasmContractCallFailed, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + // reset suite + suite.SetupWasmWithMockVM() + + endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) + err := endpoint.CreateClient() + suite.Require().NoError(err) + clientID = endpoint.ClientID + + clientState, ok := endpoint.GetClientState().(*types.ClientState) + suite.Require().True(ok) + + newLatestHeight := clienttypes.NewHeight(2, 10) + wrappedUpgradedClient := wasmtesting.CreateMockTendermintClientState(newLatestHeight) + wrappedUpgradedClientBz := clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), wrappedUpgradedClient) + upgradedClient = types.NewClientState(wrappedUpgradedClientBz, clientState.Checksum, newLatestHeight) + + wrappedUpgradedConsensus := ibctm.NewConsensusState(time.Now(), commitmenttypes.NewMerkleRoot([]byte("new-hash")), []byte("new-nextValsHash")) + wrappedUpgradedConsensusBz := clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), wrappedUpgradedConsensus) + upgradedConsState = types.NewConsensusState(wrappedUpgradedConsensusBz) + + lightClientModule, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(clientID) + suite.Require().True(found) + + tc.malleate() + + clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), wasmClientID) + + upgradedClientProof = wasmtesting.MockUpgradedClientStateProofBz + upgradedConsensusStateProof = wasmtesting.MockUpgradedConsensusStateProofBz + + newClient := suite.chainA.Codec.MustMarshal(upgradedClient) + newConsensusState := suite.chainA.Codec.MustMarshal(upgradedConsState) + + err = lightClientModule.VerifyUpgradeAndUpdateState( + suite.chainA.GetContext(), + clientID, + newClient, + newConsensusState, + upgradedClientProof, + upgradedConsensusStateProof, + ) + + expPass := tc.expErr == nil + if expPass { + suite.Require().NoError(err) + + // verify new client state and consensus state + clientStateBz := clientStore.Get(host.ClientStateKey()) + suite.Require().NotEmpty(clientStateBz) + suite.Require().Equal(upgradedClient, clienttypes.MustUnmarshalClientState(suite.chainA.Codec, clientStateBz)) + + consStateBz := clientStore.Get(host.ConsensusStateKey(upgradedClient.(*types.ClientState).LatestHeight)) + suite.Require().NotEmpty(consStateBz) + suite.Require().Equal(upgradedConsState, clienttypes.MustUnmarshalConsensusState(suite.chainA.Codec, consStateBz)) + } else { + suite.Require().Error(err) + suite.Require().ErrorIs(err, tc.expErr) + } + }) + } +} + +func (suite *WasmTestSuite) TestCheckForMisbehaviour() { + var ( + clientMessage exported.ClientMessage + clientID string + ) + + testCases := []struct { + name string + malleate func() + foundMisbehaviour bool + expPanic error + }{ + { + "success: no misbehaviour", + func() { + suite.mockVM.RegisterQueryCallback(types.CheckForMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { + resp, err := json.Marshal(types.CheckForMisbehaviourResult{FoundMisbehaviour: false}) + suite.Require().NoError(err) + return &wasmvmtypes.QueryResult{Ok: resp}, wasmtesting.DefaultGasUsed, nil + }) + }, + false, + nil, + }, + { + "success: misbehaviour found", func() { + suite.mockVM.RegisterQueryCallback(types.CheckForMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { + resp, err := json.Marshal(types.CheckForMisbehaviourResult{FoundMisbehaviour: true}) + suite.Require().NoError(err) + return &wasmvmtypes.QueryResult{Ok: resp}, wasmtesting.DefaultGasUsed, nil + }) + }, + true, + nil, + }, + { + "success: contract error, resp cannot be marshalled", func() { + suite.mockVM.RegisterQueryCallback(types.CheckForMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { + resp := "cannot be unmarshalled" + return &wasmvmtypes.QueryResult{Ok: []byte(resp)}, wasmtesting.DefaultGasUsed, nil + }) + }, + false, + nil, + }, + { + "success: contract returns error", func() { + suite.mockVM.RegisterQueryCallback(types.CheckForMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { + return &wasmvmtypes.QueryResult{Err: wasmtesting.ErrMockContract.Error()}, wasmtesting.DefaultGasUsed, nil + }) + }, + false, + nil, + }, + { + "success: vm returns error, ", func() { + suite.mockVM.RegisterQueryCallback(types.CheckForMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { + return nil, 0, errors.New("invalid block ID") + }) + }, + false, + nil, + }, + { + "success: invalid client message", func() { + clientMessage = &ibctm.Header{} + // we will not register the callback here because this test case does not reach the VM + }, + false, + nil, + }, + { + "failure: cannot find client state", + func() { + clientID = unusedWasmClientID + }, + false, // not applicable + fmt.Errorf("%s: %s", unusedWasmClientID, clienttypes.ErrClientNotFound), + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + // reset suite to create fresh application state + suite.SetupWasmWithMockVM() + + endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) + err := endpoint.CreateClient() + suite.Require().NoError(err) + clientID = endpoint.ClientID + + clientMessage = &types.ClientMessage{ + Data: clienttypes.MustMarshalClientMessage(suite.chainA.App.AppCodec(), wasmtesting.MockTendermintClientMisbehaviour), + } + + lightClientModule, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(clientID) + suite.Require().True(found) + + tc.malleate() + + var foundMisbehaviour bool + foundMisbehaviourFunc := func() { + foundMisbehaviour = lightClientModule.CheckForMisbehaviour(suite.chainA.GetContext(), clientID, clientMessage) + } + + if tc.expPanic == nil { + foundMisbehaviourFunc() + suite.Require().Equal(tc.foundMisbehaviour, foundMisbehaviour) + } else { + suite.Require().PanicsWithError(tc.expPanic.Error(), foundMisbehaviourFunc) + } + }) + } +} + +func (suite *WasmTestSuite) TestUpdateState() { + mockHeight := clienttypes.NewHeight(1, 50) + + var ( + clientMsg exported.ClientMessage + expectedClientStateBz []byte + clientID string + ) + + testCases := []struct { + name string + malleate func() + expPanic error + expHeights []exported.Height + }{ + { + "success: no update", + func() { + suite.mockVM.RegisterSudoCallback(types.UpdateStateMsg{}, func(_ wasmvm.Checksum, env wasmvmtypes.Env, sudoMsg []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + var msg types.SudoMsg + err := json.Unmarshal(sudoMsg, &msg) + suite.Require().NoError(err) + + suite.Require().NotNil(msg.UpdateState) + suite.Require().NotNil(msg.UpdateState.ClientMessage) + suite.Require().Equal(msg.UpdateState.ClientMessage, clienttypes.MustMarshalClientMessage(suite.chainA.App.AppCodec(), wasmtesting.MockTendermintClientHeader)) + suite.Require().Nil(msg.VerifyMembership) + suite.Require().Nil(msg.VerifyNonMembership) + suite.Require().Nil(msg.UpdateStateOnMisbehaviour) + suite.Require().Nil(msg.VerifyUpgradeAndUpdateState) + + suite.Require().Equal(env.Contract.Address, wasmClientID) + + updateStateResp := types.UpdateStateResult{ + Heights: []clienttypes.Height{}, + } + + resp, err := json.Marshal(updateStateResp) + if err != nil { + return nil, 0, err + } + + return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: resp}}, wasmtesting.DefaultGasUsed, nil + }) + }, + nil, + []exported.Height{}, + }, + { + "success: update client", + func() { + suite.mockVM.RegisterSudoCallback(types.UpdateStateMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + var msg types.SudoMsg + err := json.Unmarshal(sudoMsg, &msg) + suite.Require().NoError(err) + + bz := store.Get(host.ClientStateKey()) + suite.Require().NotEmpty(bz) + clientState, ok := clienttypes.MustUnmarshalClientState(suite.chainA.Codec, bz).(*types.ClientState) + suite.Require().True(ok) + clientState.LatestHeight = mockHeight + expectedClientStateBz = clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), clientState) + store.Set(host.ClientStateKey(), expectedClientStateBz) + + updateStateResp := types.UpdateStateResult{ + Heights: []clienttypes.Height{mockHeight}, + } + + resp, err := json.Marshal(updateStateResp) + if err != nil { + return nil, 0, err + } + + return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: resp}}, wasmtesting.DefaultGasUsed, nil + }) + }, + nil, + []exported.Height{mockHeight}, + }, + { + "failure: cannot find client state", + func() { + clientID = unusedWasmClientID + }, + fmt.Errorf("08-wasm-100: %s", clienttypes.ErrClientNotFound), + nil, + }, + { + "failure: invalid ClientMessage type", + func() { + // SudoCallback left nil because clientMsg is checked by 08-wasm before callbackFn is called. + clientMsg = &ibctm.Misbehaviour{} + }, + fmt.Errorf("expected type %T, got %T", (*types.ClientMessage)(nil), (*ibctm.Misbehaviour)(nil)), + nil, + }, + { + "failure: VM returns error", + func() { + suite.mockVM.RegisterSudoCallback(types.UpdateStateMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + return nil, 0, wasmtesting.ErrMockVM + }) + }, + errorsmod.Wrap(types.ErrVMError, wasmtesting.ErrMockVM.Error()), + nil, + }, + { + "failure: callbackFn returns error", + func() { + suite.mockVM.RegisterSudoCallback(types.UpdateStateMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + return &wasmvmtypes.ContractResult{Err: wasmtesting.ErrMockContract.Error()}, 0, nil + }) + }, + errorsmod.Wrap(types.ErrWasmContractCallFailed, wasmtesting.ErrMockContract.Error()), + nil, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() // reset + + endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) + err := endpoint.CreateClient() + suite.Require().NoError(err) + clientID = endpoint.ClientID + + expectedClientStateBz = nil + + clientMsg = &types.ClientMessage{ + Data: clienttypes.MustMarshalClientMessage(suite.chainA.App.AppCodec(), wasmtesting.MockTendermintClientHeader), + } + + lightClientModule, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(clientID) + suite.Require().True(found) + + tc.malleate() + + var heights []exported.Height + updateState := func() { + heights = lightClientModule.UpdateState(suite.chainA.GetContext(), clientID, clientMsg) + } + + if tc.expPanic == nil { + updateState() + suite.Require().Equal(tc.expHeights, heights) + + if expectedClientStateBz != nil { + clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), endpoint.ClientID) + + clientStateBz := clientStore.Get(host.ClientStateKey()) + suite.Require().Equal(expectedClientStateBz, clientStateBz) + } + } else { + suite.Require().PanicsWithError(tc.expPanic.Error(), updateState) + } + }) + } +} + +func (suite *WasmTestSuite) TestUpdateStateOnMisbehaviour() { + mockHeight := clienttypes.NewHeight(1, 50) + + var ( + clientMsg exported.ClientMessage + expectedClientStateBz []byte + clientID string + ) + + testCases := []struct { + name string + malleate func() + panicErr error + updatedClientState []byte + }{ + { + "success: no update", + func() { + suite.mockVM.RegisterSudoCallback(types.UpdateStateOnMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + var msg types.SudoMsg + + err := json.Unmarshal(sudoMsg, &msg) + suite.Require().NoError(err) + + suite.Require().NotNil(msg.UpdateStateOnMisbehaviour) + suite.Require().NotNil(msg.UpdateStateOnMisbehaviour.ClientMessage) + suite.Require().Nil(msg.UpdateState) + suite.Require().Nil(msg.UpdateState) + suite.Require().Nil(msg.VerifyMembership) + suite.Require().Nil(msg.VerifyNonMembership) + suite.Require().Nil(msg.VerifyUpgradeAndUpdateState) + + resp, err := json.Marshal(types.EmptyResult{}) + if err != nil { + return nil, 0, err + } + + return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: resp}}, wasmtesting.DefaultGasUsed, nil + }) + }, + nil, + nil, + }, + { + "success: client state updated on valid misbehaviour", + func() { + suite.mockVM.RegisterSudoCallback(types.UpdateStateOnMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + var msg types.SudoMsg + err := json.Unmarshal(sudoMsg, &msg) + suite.Require().NoError(err) + + // set new client state in store + bz := store.Get(host.ClientStateKey()) + suite.Require().NotEmpty(bz) + clientState, ok := clienttypes.MustUnmarshalClientState(suite.chainA.App.AppCodec(), bz).(*types.ClientState) + suite.Require().True(ok) + clientState.LatestHeight = mockHeight + expectedClientStateBz = clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), clientState) + store.Set(host.ClientStateKey(), expectedClientStateBz) + + resp, err := json.Marshal(types.EmptyResult{}) + if err != nil { + return nil, 0, err + } + + return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: resp}}, wasmtesting.DefaultGasUsed, nil + }) + }, + nil, + clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), wasmtesting.CreateMockTendermintClientState(mockHeight)), + }, + { + "failure: cannot find client state", + func() { + clientID = unusedWasmClientID + }, + fmt.Errorf("%s: %s", unusedWasmClientID, clienttypes.ErrClientNotFound), + nil, + }, + { + "failure: invalid client message", + func() { + clientMsg = &ibctm.Header{} + // we will not register the callback here because this test case does not reach the VM + }, + fmt.Errorf("expected type %T, got %T", (*types.ClientMessage)(nil), (*ibctm.Header)(nil)), + nil, + }, + { + "failure: err return from vm", + func() { + suite.mockVM.RegisterSudoCallback(types.UpdateStateOnMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + return nil, 0, wasmtesting.ErrMockVM + }) + }, + errorsmod.Wrap(types.ErrVMError, wasmtesting.ErrMockVM.Error()), + nil, + }, + { + "failure: err return from contract", + func() { + suite.mockVM.RegisterSudoCallback(types.UpdateStateOnMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + return &wasmvmtypes.ContractResult{Err: wasmtesting.ErrMockContract.Error()}, 0, nil + }) + }, + errorsmod.Wrap(types.ErrWasmContractCallFailed, wasmtesting.ErrMockContract.Error()), + nil, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + // reset suite to create fresh application state + suite.SetupWasmWithMockVM() + + endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) + err := endpoint.CreateClient() + suite.Require().NoError(err) + clientID = endpoint.ClientID + + expectedClientStateBz = nil + + clientMsg = &types.ClientMessage{ + Data: clienttypes.MustMarshalClientMessage(suite.chainA.App.AppCodec(), wasmtesting.MockTendermintClientMisbehaviour), + } + + lightClientModule, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(clientID) + suite.Require().True(found) + + tc.malleate() + + updateFunc := func() { + lightClientModule.UpdateStateOnMisbehaviour(suite.chainA.GetContext(), clientID, clientMsg) + } + + if tc.panicErr == nil { + updateFunc() + if expectedClientStateBz != nil { + store := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), endpoint.ClientID) + suite.Require().Equal(expectedClientStateBz, store.Get(host.ClientStateKey())) + } + } else { + suite.Require().PanicsWithError(tc.panicErr.Error(), updateFunc) + } + }) + } +} + +func (suite *WasmTestSuite) TestRecoverClient() { + var ( + expectedClientStateBz []byte + subjectClientID, substituteClientID string + ) + + testCases := []struct { + name string + malleate func() + expErr error + }{ + { + "success", + func() { + suite.mockVM.RegisterSudoCallback( + types.MigrateClientStoreMsg{}, + func(_ wasmvm.Checksum, _ wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + var payload types.SudoMsg + err := json.Unmarshal(sudoMsg, &payload) + suite.Require().NoError(err) + + suite.Require().NotNil(payload.MigrateClientStore) + suite.Require().Nil(payload.UpdateState) + suite.Require().Nil(payload.UpdateStateOnMisbehaviour) + suite.Require().Nil(payload.VerifyMembership) + suite.Require().Nil(payload.VerifyNonMembership) + suite.Require().Nil(payload.VerifyUpgradeAndUpdateState) + + bz, err := json.Marshal(types.EmptyResult{}) + suite.Require().NoError(err) + + // TODO(jim): re-arrange export_test.go contents + prefixedKey := []byte("subject/") + prefixedKey = append(prefixedKey, host.ClientStateKey()...) + expectedClientStateBz = wasmtesting.CreateMockClientStateBz(suite.chainA.Codec, suite.checksum) + store.Set(prefixedKey, expectedClientStateBz) + + return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: bz}}, wasmtesting.DefaultGasUsed, nil + }, + ) + }, + nil, + }, + { + "failure: cannot parse malformed substitute client ID", + func() { + substituteClientID = ibctesting.InvalidID + }, + host.ErrInvalidID, + }, + { + "failure: substitute client ID does not contain 08-wasm prefix", + func() { + substituteClientID = tmClientID + }, + clienttypes.ErrInvalidClientType, + }, + { + "failure: cannot find subject client state", + func() { + subjectClientID = unusedWasmClientID + }, + clienttypes.ErrClientNotFound, + }, + { + "failure: cannot find substitute client state", + func() { + substituteClientID = unusedWasmClientID + }, + clienttypes.ErrClientNotFound, + }, + /* TODO(jim): We don't pass a client state directly now so we need to modify how this is tested. + { + "failure: invalid substitute client state", + func() { + substituteClientState = &ibctm.ClientState{} + }, + clienttypes.ErrInvalidClient, + }, + { + "failure: checksums do not match", + func() { + substituteClientState = &types.ClientState{ + Checksum: []byte("invalid"), + } + }, + clienttypes.ErrInvalidClient, + }, + */ + { + "failure: vm returns error", + func() { + suite.mockVM.RegisterSudoCallback( + types.MigrateClientStoreMsg{}, + func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + return nil, wasmtesting.DefaultGasUsed, wasmtesting.ErrMockVM + }, + ) + }, + types.ErrVMError, + }, + { + "failure: contract returns error", + func() { + suite.mockVM.RegisterSudoCallback( + types.MigrateClientStoreMsg{}, + func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + return &wasmvmtypes.ContractResult{Err: wasmtesting.ErrMockContract.Error()}, wasmtesting.DefaultGasUsed, nil + }, + ) + }, + types.ErrWasmContractCallFailed, + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() + + subjectEndpoint := wasmtesting.NewWasmEndpoint(suite.chainA) + err := subjectEndpoint.CreateClient() + suite.Require().NoError(err) + subjectClientID = subjectEndpoint.ClientID + + substituteEndpoint := wasmtesting.NewWasmEndpoint(suite.chainA) + err = substituteEndpoint.CreateClient() + suite.Require().NoError(err) + substituteClientID = substituteEndpoint.ClientID + + expectedClientStateBz = nil + + lightClientModule, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(subjectClientID) + suite.Require().True(found) + + tc.malleate() + + err = lightClientModule.RecoverClient(suite.chainA.GetContext(), subjectClientID, substituteClientID) + + expPass := tc.expErr == nil + if expPass { + suite.Require().NoError(err) + + subjectClientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), subjectClientID) + suite.Require().Equal(expectedClientStateBz, subjectClientStore.Get(host.ClientStateKey())) + } else { + suite.Require().ErrorIs(err, tc.expErr) + } + }) + } +} + +func (suite *WasmTestSuite) TestLatestHeight() { + var clientID string + + testCases := []struct { + name string + malleate func() + expHeight clienttypes.Height + }{ + { + "success", + func() { + }, + clienttypes.NewHeight(1, 5), + }, + { + "failure: cannot find substitute client state", + func() { + clientID = unusedWasmClientID + }, + clienttypes.ZeroHeight(), + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() + + subjectEndpoint := wasmtesting.NewWasmEndpoint(suite.chainA) + err := subjectEndpoint.CreateClient() + suite.Require().NoError(err) + clientID = subjectEndpoint.ClientID + + lightClientModule, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(clientID) + suite.Require().True(found) + + tc.malleate() + + height := lightClientModule.LatestHeight(suite.chainA.GetContext(), clientID) + + suite.Require().Equal(tc.expHeight, height) }) } } diff --git a/modules/light-clients/08-wasm/types/client_state_test.go b/modules/light-clients/08-wasm/types/client_state_test.go index e5aa3c56cd4..39da76f4430 100644 --- a/modules/light-clients/08-wasm/types/client_state_test.go +++ b/modules/light-clients/08-wasm/types/client_state_test.go @@ -1,188 +1,11 @@ package types_test import ( - "encoding/json" - "time" - - wasmvm "github.com/CosmWasm/wasmvm/v2" - wasmvmtypes "github.com/CosmWasm/wasmvm/v2/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" - ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint" - 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) (*wasmvmtypes.QueryResult, uint64, error) { - resp, err := json.Marshal(types.StatusResult{Status: exported.Frozen.String()}) - suite.Require().NoError(err) - return &wasmvmtypes.QueryResult{Ok: 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) (*wasmvmtypes.QueryResult, uint64, error) { - resp, err := json.Marshal(types.StatusResult{Status: exported.Expired.String()}) - suite.Require().NoError(err) - return &wasmvmtypes.QueryResult{Ok: 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) (*wasmvmtypes.QueryResult, 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, ok := endpoint.GetClientState().(*types.ClientState) - suite.Require().True(ok) - - 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) (*wasmvmtypes.QueryResult, 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.VerifyClientMessage) - - resp, err := json.Marshal(types.TimestampAtHeightResult{Timestamp: expectedTimestamp}) - suite.Require().NoError(err) - - return &wasmvmtypes.QueryResult{Ok: resp}, wasmtesting.DefaultGasUsed, nil - }) - }, - nil, - }, - { - "failure: vm 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) (*wasmvmtypes.QueryResult, uint64, error) { - return nil, 0, wasmtesting.ErrMockVM - }) - }, - types.ErrVMError, - }, - { - "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) (*wasmvmtypes.QueryResult, uint64, error) { - return &wasmvmtypes.QueryResult{Err: wasmtesting.ErrMockContract.Error()}, 0, nil - }) - }, - 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, ok := endpoint.GetClientState().(*types.ClientState) - suite.Require().True(ok) - - height = clientState.LatestHeight - - 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 @@ -241,423 +64,3 @@ func (suite *TypesTestSuite) TestValidate() { }) } } - -func (suite *TypesTestSuite) TestInitialize() { - var ( - consensusState exported.ConsensusState - clientState *types.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.ContractResult, uint64, error) { - var payload types.InstantiateMessage - err := json.Unmarshal(initMsg, &payload) - suite.Require().NoError(err) - - suite.Require().Equal(env.Contract.Address, defaultWasmClientID) - - wrappedClientState := clienttypes.MustUnmarshalClientState(suite.chainA.App.AppCodec(), payload.ClientState).(*ibctm.ClientState) - - clientState := types.NewClientState(payload.ClientState, payload.Checksum, wrappedClientState.LatestHeight) - clientStateBz := clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), clientState) - store.Set(host.ClientStateKey(), clientStateBz) - - consensusState := types.NewConsensusState(payload.ConsensusState) - consensusStateBz := clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), consensusState) - store.Set(host.ConsensusStateKey(clientState.LatestHeight), consensusStateBz) - - resp, err := json.Marshal(types.EmptyResult{}) - suite.Require().NoError(err) - - return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: resp}}, 0, nil - } - }, - nil, - }, - { - "failure: clientStore prefix does not include clientID", - func() { - clientStore = suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), ibctesting.InvalidID) - }, - types.ErrVMError, - }, - { - "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: vm 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.ContractResult, uint64, error) { - return nil, 0, wasmtesting.ErrMockVM - } - }, - types.ErrVMError, - }, - { - "failure: contract 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.ContractResult, uint64, error) { - return &wasmvmtypes.ContractResult{Err: wasmtesting.ErrMockContract.Error()}, 0, nil - } - }, - types.ErrWasmContractCallFailed, - }, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupWasmWithMockVM() - - wrappedClientStateBz := clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), wasmtesting.MockTendermitClientState) - wrappedClientConsensusStateBz := clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), wasmtesting.MockTendermintClientConsensusState) - clientState = types.NewClientState(wrappedClientStateBz, suite.checksum, wasmtesting.MockTendermitClientState.LatestHeight) - consensusState = types.NewConsensusState(wrappedClientConsensusStateBz) - - 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.LatestHeight))) - } else { - suite.Require().ErrorIs(err, tc.expError) - } - }) - } -} - -func (suite *TypesTestSuite) TestVerifyMembership() { - var ( - clientState *types.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.ContractResult, 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.ContractResult{Ok: &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.ContractResult, 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.ContractResult{Ok: &wasmvmtypes.Response{Data: bz}}, wasmtesting.DefaultGasUsed, nil - }) - }, - nil, - }, - { - "contract 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.ContractResult, uint64, error) { - return &wasmvmtypes.ContractResult{Err: commitmenttypes.ErrInvalidProof.Error()}, wasmtesting.DefaultGasUsed, nil - }) - }, - 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() { - var ok bool - 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, ok = endpoint.GetClientState().(*types.ClientState) - suite.Require().True(ok) - - 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 *types.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.ContractResult, 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.ContractResult{Ok: &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.ContractResult, 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.ContractResult{Ok: &wasmvmtypes.Response{Data: bz}}, wasmtesting.DefaultGasUsed, nil - }) - }, - nil, - }, - { - "wasm vm returns 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.ContractResult, uint64, error) { - return nil, wasmtesting.DefaultGasUsed, wasmtesting.ErrMockVM - }) - }, - types.ErrVMError, - }, - { - "contract 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.ContractResult, uint64, error) { - return &wasmvmtypes.ContractResult{Err: commitmenttypes.ErrInvalidProof.Error()}, wasmtesting.DefaultGasUsed, nil - }) - }, - 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() { - var ok bool - 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, ok = endpoint.GetClientState().(*types.ClientState) - suite.Require().True(ok) - - 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/misbehaviour_handle_test.go b/modules/light-clients/08-wasm/types/misbehaviour_handle_test.go deleted file mode 100644 index 22acb9baf6b..00000000000 --- a/modules/light-clients/08-wasm/types/misbehaviour_handle_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package types_test - -import ( - "encoding/json" - "errors" - - wasmvm "github.com/CosmWasm/wasmvm/v2" - wasmvmtypes "github.com/CosmWasm/wasmvm/v2/types" - - 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" - "github.com/cosmos/ibc-go/v8/modules/core/exported" - ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint" -) - -func (suite *TypesTestSuite) TestCheckForMisbehaviour() { - var clientMessage exported.ClientMessage - - testCases := []struct { - name string - malleate func() - expFoundMisbehaviour bool - }{ - { - "success: no misbehaviour", - func() { - suite.mockVM.RegisterQueryCallback(types.CheckForMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { - resp, err := json.Marshal(types.CheckForMisbehaviourResult{FoundMisbehaviour: false}) - suite.Require().NoError(err) - return &wasmvmtypes.QueryResult{Ok: resp}, wasmtesting.DefaultGasUsed, nil - }) - }, - false, - }, - { - "success: misbehaviour found", func() { - suite.mockVM.RegisterQueryCallback(types.CheckForMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { - resp, err := json.Marshal(types.CheckForMisbehaviourResult{FoundMisbehaviour: true}) - suite.Require().NoError(err) - return &wasmvmtypes.QueryResult{Ok: resp}, wasmtesting.DefaultGasUsed, nil - }) - }, - true, - }, - { - "success: contract error, resp cannot be marshalled", func() { - suite.mockVM.RegisterQueryCallback(types.CheckForMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { - resp := "cannot be unmarshalled" - return &wasmvmtypes.QueryResult{Ok: []byte(resp)}, wasmtesting.DefaultGasUsed, nil - }) - }, - false, - }, - { - "success: contract returns error", func() { - suite.mockVM.RegisterQueryCallback(types.CheckForMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { - return &wasmvmtypes.QueryResult{Err: wasmtesting.ErrMockContract.Error()}, wasmtesting.DefaultGasUsed, nil - }) - }, - false, - }, - { - "success: vm returns error, ", func() { - suite.mockVM.RegisterQueryCallback(types.CheckForMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { - return nil, 0, errors.New("invalid block ID") - }) - }, - false, - }, - { - "success: invalid client message", func() { - clientMessage = &ibctm.Header{} - // we will not register the callback here because this test case does not reach the VM - }, - false, - }, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - // reset suite to create fresh application state - suite.SetupWasmWithMockVM() - endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) - err := endpoint.CreateClient() - suite.Require().NoError(err) - - clientState, ok := endpoint.GetClientState().(*types.ClientState) - suite.Require().True(ok) - clientMessage = &types.ClientMessage{ - Data: clienttypes.MustMarshalClientMessage(suite.chainA.App.AppCodec(), wasmtesting.MockTendermintClientMisbehaviour), - } - - clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), endpoint.ClientID) - - tc.malleate() - - foundMisbehaviour := clientState.CheckForMisbehaviour(suite.chainA.GetContext(), suite.chainA.App.AppCodec(), clientStore, clientMessage) - suite.Require().Equal(tc.expFoundMisbehaviour, foundMisbehaviour) - }) - } -} diff --git a/modules/light-clients/08-wasm/types/proposal_handle_test.go b/modules/light-clients/08-wasm/types/proposal_handle_test.go deleted file mode 100644 index 2e426307494..00000000000 --- a/modules/light-clients/08-wasm/types/proposal_handle_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package types_test - -import ( - "encoding/json" - - cosmwasm "github.com/CosmWasm/wasmvm/v2" - wasmvmtypes "github.com/CosmWasm/wasmvm/v2/types" - - 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" - "github.com/cosmos/ibc-go/v8/modules/core/exported" - ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint" -) - -func (suite *TypesTestSuite) TestCheckSubstituteAndUpdateState() { - var substituteClientState exported.ClientState - var expectedClientStateBz []byte - - testCases := []struct { - name string - malleate func() - expErr error - }{ - { - "success", - func() { - suite.mockVM.RegisterSudoCallback( - types.MigrateClientStoreMsg{}, - func(_ cosmwasm.Checksum, _ wasmvmtypes.Env, sudoMsg []byte, store cosmwasm.KVStore, _ cosmwasm.GoAPI, _ cosmwasm.Querier, _ cosmwasm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { - var payload types.SudoMsg - err := json.Unmarshal(sudoMsg, &payload) - suite.Require().NoError(err) - - suite.Require().NotNil(payload.MigrateClientStore) - suite.Require().Nil(payload.UpdateState) - suite.Require().Nil(payload.UpdateStateOnMisbehaviour) - suite.Require().Nil(payload.VerifyMembership) - suite.Require().Nil(payload.VerifyNonMembership) - suite.Require().Nil(payload.VerifyUpgradeAndUpdateState) - - bz, err := json.Marshal(types.EmptyResult{}) - suite.Require().NoError(err) - - prefixedKey := types.SubjectPrefix - prefixedKey = append(prefixedKey, host.ClientStateKey()...) - expectedClientStateBz = wasmtesting.CreateMockClientStateBz(suite.chainA.Codec, suite.checksum) - store.Set(prefixedKey, expectedClientStateBz) - - return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: bz}}, wasmtesting.DefaultGasUsed, nil - }, - ) - }, - nil, - }, - { - "failure: invalid substitute client state", - func() { - substituteClientState = &ibctm.ClientState{} - }, - clienttypes.ErrInvalidClient, - }, - { - "failure: checksums do not match", - func() { - substituteClientState = &types.ClientState{ - Checksum: []byte("invalid"), - } - }, - clienttypes.ErrInvalidClient, - }, - { - "failure: vm returns error", - func() { - suite.mockVM.RegisterSudoCallback( - types.MigrateClientStoreMsg{}, - func(_ cosmwasm.Checksum, _ wasmvmtypes.Env, _ []byte, _ cosmwasm.KVStore, _ cosmwasm.GoAPI, _ cosmwasm.Querier, _ cosmwasm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { - return nil, wasmtesting.DefaultGasUsed, wasmtesting.ErrMockVM - }, - ) - }, - types.ErrVMError, - }, - { - "failure: contract returns error", - func() { - suite.mockVM.RegisterSudoCallback( - types.MigrateClientStoreMsg{}, - func(_ cosmwasm.Checksum, _ wasmvmtypes.Env, _ []byte, _ cosmwasm.KVStore, _ cosmwasm.GoAPI, _ cosmwasm.Querier, _ cosmwasm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { - return &wasmvmtypes.ContractResult{Err: wasmtesting.ErrMockContract.Error()}, wasmtesting.DefaultGasUsed, nil - }, - ) - }, - types.ErrWasmContractCallFailed, - }, - } - - for _, tc := range testCases { - tc := tc - suite.Run(tc.name, func() { - suite.SetupWasmWithMockVM() - expectedClientStateBz = nil - - endpointA := wasmtesting.NewWasmEndpoint(suite.chainA) - err := endpointA.CreateClient() - suite.Require().NoError(err) - - subjectClientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), endpointA.ClientID) - subjectClientState, ok := endpointA.GetClientState().(*types.ClientState) - suite.Require().True(ok) - - substituteEndpoint := wasmtesting.NewWasmEndpoint(suite.chainA) - err = substituteEndpoint.CreateClient() - suite.Require().NoError(err) - - substituteClientState = substituteEndpoint.GetClientState() - substituteClientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), substituteEndpoint.ClientID) - - tc.malleate() - - err = subjectClientState.CheckSubstituteAndUpdateState( - suite.chainA.GetContext(), - suite.chainA.Codec, - subjectClientStore, - substituteClientStore, - substituteClientState, - ) - - expPass := tc.expErr == nil - if expPass { - suite.Require().NoError(err) - - clientStateBz := subjectClientStore.Get(host.ClientStateKey()) - suite.Require().Equal(expectedClientStateBz, clientStateBz) - } else { - suite.Require().ErrorIs(err, tc.expErr) - } - }) - } -} diff --git a/modules/light-clients/08-wasm/types/update_test.go b/modules/light-clients/08-wasm/types/update_test.go deleted file mode 100644 index 529b6a89954..00000000000 --- a/modules/light-clients/08-wasm/types/update_test.go +++ /dev/null @@ -1,309 +0,0 @@ -package types_test - -import ( - "encoding/json" - "fmt" - - wasmvm "github.com/CosmWasm/wasmvm/v2" - wasmvmtypes "github.com/CosmWasm/wasmvm/v2/types" - - errorsmod "cosmossdk.io/errors" - storetypes "cosmossdk.io/store/types" - - 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" - "github.com/cosmos/ibc-go/v8/modules/core/exported" - ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint" - ibctesting "github.com/cosmos/ibc-go/v8/testing" -) - -func (suite *TypesTestSuite) TestUpdateState() { - mockHeight := clienttypes.NewHeight(1, 50) - - var ( - clientMsg exported.ClientMessage - clientStore storetypes.KVStore - expectedClientStateBz []byte - ) - - testCases := []struct { - name string - malleate func() - expPanic error - expHeights []exported.Height - }{ - { - "success: no update", - func() { - suite.mockVM.RegisterSudoCallback(types.UpdateStateMsg{}, func(_ wasmvm.Checksum, env wasmvmtypes.Env, sudoMsg []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { - var msg types.SudoMsg - err := json.Unmarshal(sudoMsg, &msg) - suite.Require().NoError(err) - - suite.Require().NotNil(msg.UpdateState) - suite.Require().NotNil(msg.UpdateState.ClientMessage) - suite.Require().Equal(msg.UpdateState.ClientMessage, clienttypes.MustMarshalClientMessage(suite.chainA.App.AppCodec(), wasmtesting.MockTendermintClientHeader)) - suite.Require().Nil(msg.VerifyMembership) - suite.Require().Nil(msg.VerifyNonMembership) - suite.Require().Nil(msg.UpdateStateOnMisbehaviour) - suite.Require().Nil(msg.VerifyUpgradeAndUpdateState) - - suite.Require().Equal(env.Contract.Address, defaultWasmClientID) - - updateStateResp := types.UpdateStateResult{ - Heights: []clienttypes.Height{}, - } - - resp, err := json.Marshal(updateStateResp) - if err != nil { - return nil, 0, err - } - - return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: resp}}, wasmtesting.DefaultGasUsed, nil - }) - }, - nil, - []exported.Height{}, - }, - { - "success: update client", - func() { - suite.mockVM.RegisterSudoCallback(types.UpdateStateMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { - var msg types.SudoMsg - err := json.Unmarshal(sudoMsg, &msg) - suite.Require().NoError(err) - - bz := store.Get(host.ClientStateKey()) - suite.Require().NotEmpty(bz) - clientState, ok := clienttypes.MustUnmarshalClientState(suite.chainA.Codec, bz).(*types.ClientState) - suite.Require().True(ok) - clientState.LatestHeight = mockHeight - expectedClientStateBz = clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), clientState) - store.Set(host.ClientStateKey(), expectedClientStateBz) - - updateStateResp := types.UpdateStateResult{ - Heights: []clienttypes.Height{mockHeight}, - } - - resp, err := json.Marshal(updateStateResp) - if err != nil { - return nil, 0, err - } - - return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: resp}}, wasmtesting.DefaultGasUsed, nil - }) - }, - nil, - []exported.Height{mockHeight}, - }, - { - "failure: clientStore prefix does not include clientID", - func() { - clientStore = suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), ibctesting.InvalidID) - }, - errorsmod.Wrap(types.ErrVMError, errorsmod.Wrap(errorsmod.Wrapf(types.ErrRetrieveClientID, "prefix does not contain a valid clientID: %s", errorsmod.Wrapf(host.ErrInvalidID, "invalid client identifier %s", ibctesting.InvalidID)), "failed to retrieve clientID for wasm contract call").Error()), - nil, - }, - { - "failure: invalid ClientMessage type", - func() { - // SudoCallback left nil because clientMsg is checked by 08-wasm before callbackFn is called. - clientMsg = &ibctm.Misbehaviour{} - }, - fmt.Errorf("expected type %T, got %T", (*types.ClientMessage)(nil), (*ibctm.Misbehaviour)(nil)), - nil, - }, - { - "failure: VM returns error", - func() { - suite.mockVM.RegisterSudoCallback(types.UpdateStateMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { - return nil, 0, wasmtesting.ErrMockVM - }) - }, - errorsmod.Wrap(types.ErrVMError, wasmtesting.ErrMockVM.Error()), - nil, - }, - { - "failure: callbackFn returns error", - func() { - suite.mockVM.RegisterSudoCallback(types.UpdateStateMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { - return &wasmvmtypes.ContractResult{Err: wasmtesting.ErrMockContract.Error()}, 0, nil - }) - }, - errorsmod.Wrap(types.ErrWasmContractCallFailed, wasmtesting.ErrMockContract.Error()), - nil, - }, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - suite.SetupWasmWithMockVM() // reset - expectedClientStateBz = nil - - clientMsg = &types.ClientMessage{ - Data: clienttypes.MustMarshalClientMessage(suite.chainA.App.AppCodec(), wasmtesting.MockTendermintClientHeader), - } - - endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) - err := endpoint.CreateClient() - suite.Require().NoError(err) - clientStore = suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), endpoint.ClientID) - - tc.malleate() - - clientState, ok := endpoint.GetClientState().(*types.ClientState) - suite.Require().True(ok) - - var heights []exported.Height - updateState := func() { - heights = clientState.UpdateState(suite.chainA.GetContext(), suite.chainA.Codec, clientStore, clientMsg) - } - - if tc.expPanic == nil { - updateState() - suite.Require().Equal(tc.expHeights, heights) - - if expectedClientStateBz != nil { - clientStateBz := clientStore.Get(host.ClientStateKey()) - suite.Require().Equal(expectedClientStateBz, clientStateBz) - } - } else { - suite.Require().PanicsWithError(tc.expPanic.Error(), updateState) - } - }) - } -} - -func (suite *TypesTestSuite) TestUpdateStateOnMisbehaviour() { - mockHeight := clienttypes.NewHeight(1, 50) - - var clientMsg exported.ClientMessage - - var expectedClientStateBz []byte - - testCases := []struct { - name string - malleate func() - panicErr error - updatedClientState []byte - }{ - { - "success: no update", - func() { - suite.mockVM.RegisterSudoCallback(types.UpdateStateOnMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { - var msg types.SudoMsg - - err := json.Unmarshal(sudoMsg, &msg) - suite.Require().NoError(err) - - suite.Require().NotNil(msg.UpdateStateOnMisbehaviour) - suite.Require().NotNil(msg.UpdateStateOnMisbehaviour.ClientMessage) - suite.Require().Nil(msg.UpdateState) - suite.Require().Nil(msg.UpdateState) - suite.Require().Nil(msg.VerifyMembership) - suite.Require().Nil(msg.VerifyNonMembership) - suite.Require().Nil(msg.VerifyUpgradeAndUpdateState) - - resp, err := json.Marshal(types.EmptyResult{}) - if err != nil { - return nil, 0, err - } - - return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: resp}}, wasmtesting.DefaultGasUsed, nil - }) - }, - nil, - nil, - }, - { - "success: client state updated on valid misbehaviour", - func() { - suite.mockVM.RegisterSudoCallback(types.UpdateStateOnMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { - var msg types.SudoMsg - err := json.Unmarshal(sudoMsg, &msg) - suite.Require().NoError(err) - - // set new client state in store - bz := store.Get(host.ClientStateKey()) - suite.Require().NotEmpty(bz) - clientState, ok := clienttypes.MustUnmarshalClientState(suite.chainA.App.AppCodec(), bz).(*types.ClientState) - suite.Require().True(ok) - clientState.LatestHeight = mockHeight - expectedClientStateBz = clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), clientState) - store.Set(host.ClientStateKey(), expectedClientStateBz) - - resp, err := json.Marshal(types.EmptyResult{}) - if err != nil { - return nil, 0, err - } - - return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: resp}}, wasmtesting.DefaultGasUsed, nil - }) - }, - nil, - clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), wasmtesting.CreateMockTendermintClientState(mockHeight)), - }, - { - "failure: invalid client message", - func() { - clientMsg = &ibctm.Header{} - // we will not register the callback here because this test case does not reach the VM - }, - fmt.Errorf("expected type %T, got %T", (*types.ClientMessage)(nil), (*ibctm.Header)(nil)), - nil, - }, - { - "failure: err return from vm", - func() { - suite.mockVM.RegisterSudoCallback(types.UpdateStateOnMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { - return nil, 0, wasmtesting.ErrMockVM - }) - }, - errorsmod.Wrap(types.ErrVMError, wasmtesting.ErrMockVM.Error()), - nil, - }, - { - "failure: err return from contract", - func() { - suite.mockVM.RegisterSudoCallback(types.UpdateStateOnMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { - return &wasmvmtypes.ContractResult{Err: wasmtesting.ErrMockContract.Error()}, 0, nil - }) - }, - errorsmod.Wrap(types.ErrWasmContractCallFailed, wasmtesting.ErrMockContract.Error()), - nil, - }, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - // reset suite to create fresh application state - suite.SetupWasmWithMockVM() - expectedClientStateBz = nil - - endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) - err := endpoint.CreateClient() - suite.Require().NoError(err) - - store := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), endpoint.ClientID) - clientMsg = &types.ClientMessage{ - Data: clienttypes.MustMarshalClientMessage(suite.chainA.App.AppCodec(), wasmtesting.MockTendermintClientMisbehaviour), - } - clientState, ok := endpoint.GetClientState().(*types.ClientState) - suite.Require().True(ok) - tc.malleate() - - if tc.panicErr == nil { - clientState.UpdateStateOnMisbehaviour(suite.chainA.GetContext(), suite.chainA.App.AppCodec(), store, clientMsg) - if expectedClientStateBz != nil { - suite.Require().Equal(expectedClientStateBz, store.Get(host.ClientStateKey())) - } - } else { - suite.Require().PanicsWithError(tc.panicErr.Error(), func() { - clientState.UpdateStateOnMisbehaviour(suite.chainA.GetContext(), suite.chainA.App.AppCodec(), store, clientMsg) - }) - } - }) - } -} diff --git a/modules/light-clients/08-wasm/types/upgrade_test.go b/modules/light-clients/08-wasm/types/upgrade_test.go deleted file mode 100644 index ead537ce048..00000000000 --- a/modules/light-clients/08-wasm/types/upgrade_test.go +++ /dev/null @@ -1,276 +0,0 @@ -package types_test - -import ( - "encoding/json" - "time" - - wasmvm "github.com/CosmWasm/wasmvm/v2" - wasmvmtypes "github.com/CosmWasm/wasmvm/v2/types" - - storetypes "cosmossdk.io/store/types" - - 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" - ibctm "github.com/cosmos/ibc-go/v8/modules/light-clients/07-tendermint" - ibctesting "github.com/cosmos/ibc-go/v8/testing" -) - -func (suite *TypesTestSuite) TestVerifyClientMessage() { - var ( - clientMsg exported.ClientMessage - clientStore storetypes.KVStore - ) - - testCases := []struct { - name string - malleate func() - expErr error - }{ - { - "success: valid misbehaviour", - func() { - suite.mockVM.RegisterQueryCallback(types.VerifyClientMessageMsg{}, func(_ wasmvm.Checksum, env wasmvmtypes.Env, queryMsg []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { - var msg *types.QueryMsg - - err := json.Unmarshal(queryMsg, &msg) - suite.Require().NoError(err) - - suite.Require().NotNil(msg.VerifyClientMessage) - suite.Require().NotNil(msg.VerifyClientMessage.ClientMessage) - suite.Require().Nil(msg.Status) - suite.Require().Nil(msg.CheckForMisbehaviour) - suite.Require().Nil(msg.TimestampAtHeight) - - suite.Require().Equal(env.Contract.Address, defaultWasmClientID) - - resp, err := json.Marshal(types.EmptyResult{}) - suite.Require().NoError(err) - - return &wasmvmtypes.QueryResult{Ok: resp}, wasmtesting.DefaultGasUsed, nil - }) - }, - nil, - }, - { - "failure: clientStore prefix does not include clientID", - func() { - clientStore = suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), ibctesting.InvalidID) - }, - types.ErrVMError, - }, - { - "failure: invalid client message", - func() { - clientMsg = &ibctm.Header{} - - suite.mockVM.RegisterQueryCallback(types.VerifyClientMessageMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, queryMsg []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { - resp, err := json.Marshal(types.EmptyResult{}) - suite.Require().NoError(err) - - return &wasmvmtypes.QueryResult{Ok: resp}, wasmtesting.DefaultGasUsed, nil - }) - }, - ibcerrors.ErrInvalidType, - }, - { - "failure: error return from vm", - func() { - suite.mockVM.RegisterQueryCallback(types.VerifyClientMessageMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, queryMsg []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { - return nil, 0, wasmtesting.ErrMockVM - }) - }, - types.ErrVMError, - }, - { - "failure: error return from contract", - func() { - suite.mockVM.RegisterQueryCallback(types.VerifyClientMessageMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, queryMsg []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { - return &wasmvmtypes.QueryResult{Err: wasmtesting.ErrMockContract.Error()}, 0, nil - }) - }, - types.ErrWasmContractCallFailed, - }, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - // reset suite to create fresh application state - 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, ok := endpoint.GetClientState().(*types.ClientState) - suite.Require().True(ok) - - clientMsg = &types.ClientMessage{ - Data: clienttypes.MustMarshalClientMessage(suite.chainA.App.AppCodec(), wasmtesting.MockTendermintClientHeader), - } - - tc.malleate() - - err = clientState.VerifyClientMessage(suite.chainA.GetContext(), suite.chainA.App.AppCodec(), clientStore, clientMsg) - - expPass := tc.expErr == nil - if expPass { - suite.Require().NoError(err) - } else { - suite.Require().ErrorIs(err, tc.expErr) - } - }) - } -} - -func (suite *TypesTestSuite) TestVerifyUpgradeAndUpdateState() { - var ( - upgradedClient exported.ClientState - upgradedConsState exported.ConsensusState - upgradedClientProof []byte - upgradedConsensusStateProof []byte - ) - - testCases := []struct { - name string - malleate func() - expErr error - }{ - { - "success: successful upgrade", - func() { - suite.mockVM.RegisterSudoCallback(types.VerifyUpgradeAndUpdateStateMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, sudoMsg []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { - var payload types.SudoMsg - - err := json.Unmarshal(sudoMsg, &payload) - suite.Require().NoError(err) - - expectedUpgradedClient, ok := upgradedClient.(*types.ClientState) - suite.Require().True(ok) - expectedUpgradedConsensus, ok := upgradedConsState.(*types.ConsensusState) - suite.Require().True(ok) - - // verify payload values - suite.Require().Equal(expectedUpgradedClient.Data, payload.VerifyUpgradeAndUpdateState.UpgradeClientState) - suite.Require().Equal(expectedUpgradedConsensus.Data, payload.VerifyUpgradeAndUpdateState.UpgradeConsensusState) - suite.Require().Equal(upgradedClientProof, payload.VerifyUpgradeAndUpdateState.ProofUpgradeClient) - suite.Require().Equal(upgradedConsensusStateProof, payload.VerifyUpgradeAndUpdateState.ProofUpgradeConsensusState) - - // verify other Sudo fields are nil - suite.Require().Nil(payload.UpdateState) - suite.Require().Nil(payload.UpdateStateOnMisbehaviour) - suite.Require().Nil(payload.VerifyMembership) - suite.Require().Nil(payload.VerifyNonMembership) - - data, err := json.Marshal(types.EmptyResult{}) - suite.Require().NoError(err) - - // set new client state and consensus state - wrappedUpgradedClient := clienttypes.MustUnmarshalClientState(suite.chainA.App.AppCodec(), expectedUpgradedClient.Data).(*ibctm.ClientState) - store.Set(host.ClientStateKey(), clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), upgradedClient)) - store.Set(host.ConsensusStateKey(wrappedUpgradedClient.LatestHeight), clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), upgradedConsState)) - - return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: data}}, wasmtesting.DefaultGasUsed, nil - }) - }, - nil, - }, - { - "failure: upgraded client state is not wasm client state", - func() { - // set upgraded client state to solomachine client state - upgradedClient = &solomachine.ClientState{} - }, - clienttypes.ErrInvalidClient, - }, - { - "failure: upgraded consensus state is not wasm consensus state", - func() { - // set upgraded consensus state to solomachine consensus state - upgradedConsState = &solomachine.ConsensusState{} - }, - clienttypes.ErrInvalidConsensus, - }, - { - "failure: vm returns error", - func() { - suite.mockVM.RegisterSudoCallback(types.VerifyUpgradeAndUpdateStateMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { - return nil, 0, wasmtesting.ErrMockVM - }) - }, - types.ErrVMError, - }, - { - "failure: contract returns error", - func() { - suite.mockVM.RegisterSudoCallback(types.VerifyUpgradeAndUpdateStateMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { - return &wasmvmtypes.ContractResult{Err: wasmtesting.ErrMockContract.Error()}, 0, nil - }) - }, - types.ErrWasmContractCallFailed, - }, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - // reset suite - suite.SetupWasmWithMockVM() - - endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) - err := endpoint.CreateClient() - suite.Require().NoError(err) - - clientState, ok := endpoint.GetClientState().(*types.ClientState) - suite.Require().True(ok) - - newLatestHeight := clienttypes.NewHeight(2, 10) - wrappedUpgradedClient := wasmtesting.CreateMockTendermintClientState(newLatestHeight) - wrappedUpgradedClientBz := clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), wrappedUpgradedClient) - upgradedClient = types.NewClientState(wrappedUpgradedClientBz, clientState.Checksum, newLatestHeight) - - wrappedUpgradedConsensus := ibctm.NewConsensusState(time.Now(), commitmenttypes.NewMerkleRoot([]byte("new-hash")), []byte("new-nextValsHash")) - wrappedUpgradedConsensusBz := clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), wrappedUpgradedConsensus) - upgradedConsState = types.NewConsensusState(wrappedUpgradedConsensusBz) - - tc.malleate() - - clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), defaultWasmClientID) - - upgradedClientProof = wasmtesting.MockUpgradedClientStateProofBz - upgradedConsensusStateProof = wasmtesting.MockUpgradedConsensusStateProofBz - - err = clientState.VerifyUpgradeAndUpdateState( - suite.chainA.GetContext(), - suite.chainA.Codec, - clientStore, - upgradedClient, - upgradedConsState, - upgradedClientProof, - upgradedConsensusStateProof, - ) - - expPass := tc.expErr == nil - if expPass { - suite.Require().NoError(err) - - // verify new client state and consensus state - clientStateBz := clientStore.Get(host.ClientStateKey()) - suite.Require().NotEmpty(clientStateBz) - suite.Require().Equal(upgradedClient, clienttypes.MustUnmarshalClientState(suite.chainA.Codec, clientStateBz)) - - consStateBz := clientStore.Get(host.ConsensusStateKey(upgradedClient.(*types.ClientState).LatestHeight)) - suite.Require().NotEmpty(consStateBz) - suite.Require().Equal(upgradedConsState, clienttypes.MustUnmarshalConsensusState(suite.chainA.Codec, consStateBz)) - } else { - suite.Require().Error(err) - suite.Require().ErrorIs(err, tc.expErr) - } - }) - } -} From 0b27b844db7d3cc9ce7aafc5cfd6befa530b89fc Mon Sep 17 00:00:00 2001 From: DimitrisJim Date: Wed, 10 Apr 2024 20:50:18 +0300 Subject: [PATCH 2/6] Remove usage of globals from 08-wasm (#6103) * Move functionality from client state methods to light client module methods. - Yank all statements and delete functions as needed. - Make any required functions from types/ public. - Add the vm in the 08-wasm keeper. - Update documentation to include documentation of deleted functions. * Remove global VM, introspection to retrieve client identifier. - Refactor signatures to explicitly pass vm, client identifier for time being. - Refactor tests to conform with new signatures. - Replace occurences of `ibcwasm.GetVM` in other keeper functions. * Move migrateContract from clientState to keeper method. - Matches moves for other functions. - Move is required to address future circular dependency for state management (keeper would import types and types would import keeper) - Rm migrate_contract(test).go files. * Move state management to keeper. - Remove globals for state (Checksums) - Move access functions from types to keeper, move tests. - Update calls using global to instead go through keeper. * Make InitializePinnedCodes a method on the keeper. * Remove pending todo in test for migrate contract. This previously was testing a function that didn't set the client state after calling the contract (invoking through the keeper always sets a client state after contract invocation). Hence, even if we don't set explicitly in the migrateFn callback, we _still_ get a client state in the end. * Move vm entrypoint functions in keeper/ package. * feat: move vm entry points to keeper. * chore: simplify signatures. * test: add amended tests for contact keeper. * refactor: move querier to keeper package. * lint: you'll never fix me. * refactor: remove queryRouter global variable - Move into the func for creating new plugins, pass it to acceptStargateQuery directly. This is probably what wasmd did (see they don't accept a query router now?). Since we don't use the get elsewhere, this was the smallest diff for a globa. - Preemptively ran make lint-fix, gotcha linting crybaby. * refactor: remove global for query plugins. * nit: rename to queryPlugins. * refactor: remove QueryPluginsI interface. * refactor: make queryHandler a private type. * Make migrateContractCode a private function, clean up uneeded export in export_test.go for types/. * refactor: Move vm entrypoints to keeper. - Remove generic types and do unmarshalling in light client methods. - Move all functions onto keeper and adjust call sites. * chore: address some testing todos. Move testing unmarshal failure case to light client tests. * chore: additional tiny nits. - Consistently use wasmClientKeeper as local var name. - Reorg keeper fields slightly. - Rm 'vm.go' which was already empty. * chore: restructure definitions to make diff more readable. * d'oh: rm vm arg from instantiate. * nit: Space out keeper fields. Remove TODO for method name. --- .../internal/ibcwasm/expected_interfaces.go | 6 - .../08-wasm/internal/ibcwasm/wasm.go | 77 ----- .../vm.go => keeper/contract_keeper.go} | 160 +++++----- .../contract_keeper_test.go} | 46 ++- .../08-wasm/keeper/export_test.go | 8 + .../light-clients/08-wasm/keeper/genesis.go | 9 +- .../08-wasm/keeper/genesis_test.go | 2 +- .../08-wasm/keeper/grpc_query.go | 11 +- .../light-clients/08-wasm/keeper/keeper.go | 113 +++++++- .../08-wasm/keeper/keeper_test.go | 252 +++++++++++++++- .../light-clients/08-wasm/keeper/keeper_vm.go | 24 +- .../08-wasm/keeper/migrations.go | 5 +- .../08-wasm/keeper/migrations_test.go | 7 +- .../08-wasm/keeper/msg_server.go | 9 +- .../08-wasm/keeper/msg_server_test.go | 6 +- .../light-clients/08-wasm/keeper/options.go | 19 +- .../08-wasm/keeper/options_test.go | 39 ++- .../08-wasm/{types => keeper}/querier.go | 26 +- .../08-wasm/{types => keeper}/querier_test.go | 89 ++++-- .../08-wasm/keeper/snapshotter.go | 9 +- .../08-wasm/light_client_module.go | 274 ++++++++++++++++-- .../08-wasm/light_client_module_test.go | 42 ++- .../08-wasm/testing/simapp/app.go | 2 +- .../08-wasm/types/client_state.go | 170 ----------- modules/light-clients/08-wasm/types/events.go | 13 - .../08-wasm/types/export_test.go | 42 +-- .../08-wasm/types/gas_register.go | 11 +- .../08-wasm/types/gas_register_custom.go | 2 +- modules/light-clients/08-wasm/types/keys.go | 5 + .../08-wasm/types/migrate_contract.go | 41 --- .../08-wasm/types/migrate_contract_test.go | 153 ---------- .../08-wasm/types/misbehaviour_handle.go | 30 -- .../08-wasm/types/proposal_handle.go | 42 --- modules/light-clients/08-wasm/types/store.go | 156 +++------- .../light-clients/08-wasm/types/store_test.go | 206 +------------ modules/light-clients/08-wasm/types/update.go | 75 ----- .../light-clients/08-wasm/types/upgrade.go | 48 --- 37 files changed, 929 insertions(+), 1300 deletions(-) delete mode 100644 modules/light-clients/08-wasm/internal/ibcwasm/wasm.go rename modules/light-clients/08-wasm/{types/vm.go => keeper/contract_keeper.go} (51%) rename modules/light-clients/08-wasm/{types/vm_test.go => keeper/contract_keeper_test.go} (92%) create mode 100644 modules/light-clients/08-wasm/keeper/export_test.go rename modules/light-clients/08-wasm/{types => keeper}/querier.go (87%) rename modules/light-clients/08-wasm/{types => keeper}/querier_test.go (80%) delete mode 100644 modules/light-clients/08-wasm/types/migrate_contract.go delete mode 100644 modules/light-clients/08-wasm/types/migrate_contract_test.go delete mode 100644 modules/light-clients/08-wasm/types/misbehaviour_handle.go delete mode 100644 modules/light-clients/08-wasm/types/proposal_handle.go delete mode 100644 modules/light-clients/08-wasm/types/update.go delete mode 100644 modules/light-clients/08-wasm/types/upgrade.go diff --git a/modules/light-clients/08-wasm/internal/ibcwasm/expected_interfaces.go b/modules/light-clients/08-wasm/internal/ibcwasm/expected_interfaces.go index e03c156071a..ea000c4f26b 100644 --- a/modules/light-clients/08-wasm/internal/ibcwasm/expected_interfaces.go +++ b/modules/light-clients/08-wasm/internal/ibcwasm/expected_interfaces.go @@ -5,7 +5,6 @@ import ( wasmvmtypes "github.com/CosmWasm/wasmvm/v2/types" "github.com/cosmos/cosmos-sdk/baseapp" - sdk "github.com/cosmos/cosmos-sdk/types" ) type WasmEngine interface { @@ -123,8 +122,3 @@ type QueryRouter interface { // if not found Route(path string) baseapp.GRPCQueryHandler } - -type QueryPluginsI interface { - // HandleQuery will route the query to the correct plugin and return the result - HandleQuery(ctx sdk.Context, caller string, request wasmvmtypes.QueryRequest) ([]byte, error) -} diff --git a/modules/light-clients/08-wasm/internal/ibcwasm/wasm.go b/modules/light-clients/08-wasm/internal/ibcwasm/wasm.go deleted file mode 100644 index 0b4fa20ba5b..00000000000 --- a/modules/light-clients/08-wasm/internal/ibcwasm/wasm.go +++ /dev/null @@ -1,77 +0,0 @@ -package ibcwasm - -import ( - "errors" - - "cosmossdk.io/collections" - storetypes "cosmossdk.io/core/store" -) - -var ( - vm WasmEngine - - queryRouter QueryRouter - queryPlugins QueryPluginsI - - // state management - Schema collections.Schema - Checksums collections.KeySet[[]byte] - - // ChecksumsKey is the key under which all checksums are stored - ChecksumsKey = collections.NewPrefix(0) -) - -// SetVM sets the wasm VM for the 08-wasm module. -// It panics if the wasm VM is nil. -func SetVM(wasmVM WasmEngine) { - if wasmVM == nil { - panic(errors.New("wasm VM must be not nil")) - } - vm = wasmVM -} - -// GetVM returns the wasm VM for the 08-wasm module. -func GetVM() WasmEngine { - return vm -} - -// SetQueryRouter sets the custom wasm query router for the 08-wasm module. -// Panics if the queryRouter is nil. -func SetQueryRouter(router QueryRouter) { - if router == nil { - panic(errors.New("query router must not be nil")) - } - queryRouter = router -} - -// GetQueryRouter returns the custom wasm query router for the 08-wasm module. -func GetQueryRouter() QueryRouter { - return queryRouter -} - -// SetQueryPlugins sets the current query plugins -func SetQueryPlugins(plugins QueryPluginsI) { - if plugins == nil { - panic(errors.New("query plugins must not be nil")) - } - queryPlugins = plugins -} - -// GetQueryPlugins returns the current query plugins -func GetQueryPlugins() QueryPluginsI { - return queryPlugins -} - -// SetupWasmStoreService sets up the 08-wasm module's collections. -func SetupWasmStoreService(storeService storetypes.KVStoreService) { - sb := collections.NewSchemaBuilder(storeService) - - Checksums = collections.NewKeySet(sb, ChecksumsKey, "checksums", collections.BytesKey) - - schema, err := sb.Build() - if err != nil { - panic(err) - } - - Schema = schema -} diff --git a/modules/light-clients/08-wasm/types/vm.go b/modules/light-clients/08-wasm/keeper/contract_keeper.go similarity index 51% rename from modules/light-clients/08-wasm/types/vm.go rename to modules/light-clients/08-wasm/keeper/contract_keeper.go index 70115968cea..9fd16dff6e3 100644 --- a/modules/light-clients/08-wasm/types/vm.go +++ b/modules/light-clients/08-wasm/keeper/contract_keeper.go @@ -1,4 +1,4 @@ -package types +package keeper import ( "bytes" @@ -15,14 +15,14 @@ import ( "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" + "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" ) var ( - VMGasRegister = NewDefaultWasmGasRegister() + VMGasRegister = types.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{ @@ -33,15 +33,11 @@ var ( ) // instantiateContract calls vm.Instantiate with appropriate arguments. -func instantiateContract(ctx sdk.Context, clientStore storetypes.KVStore, checksum Checksum, msg []byte) (*wasmvmtypes.ContractResult, error) { +func (k Keeper) instantiateContract(ctx sdk.Context, clientID string, clientStore storetypes.KVStore, checksum types.Checksum, msg []byte) (*wasmvmtypes.ContractResult, error) { sdkGasMeter := ctx.GasMeter() - multipliedGasMeter := NewMultipliedGasMeter(sdkGasMeter, VMGasRegister) + multipliedGasMeter := types.NewMultipliedGasMeter(sdkGasMeter, types.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{ @@ -49,187 +45,167 @@ func instantiateContract(ctx sdk.Context, clientStore storetypes.KVStore, checks Funds: nil, } - ctx.GasMeter().ConsumeGas(VMGasRegister.SetupContractCost(true, len(msg)), "Loading CosmWasm module: instantiate") - resp, gasUsed, err := ibcwasm.GetVM().Instantiate(checksum, env, msgInfo, msg, newStoreAdapter(clientStore), wasmvmAPI, newQueryHandler(ctx, clientID), multipliedGasMeter, gasLimit, costJSONDeserialization) - VMGasRegister.ConsumeRuntimeGas(ctx, gasUsed) + ctx.GasMeter().ConsumeGas(types.VMGasRegister.SetupContractCost(true, len(msg)), "Loading CosmWasm module: instantiate") + resp, gasUsed, err := k.GetVM().Instantiate(checksum, env, msgInfo, msg, types.NewStoreAdapter(clientStore), wasmvmAPI, k.newQueryHandler(ctx, clientID), multipliedGasMeter, gasLimit, types.CostJSONDeserialization) + types.VMGasRegister.ConsumeRuntimeGas(ctx, gasUsed) return resp, 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.ContractResult, error) { +func (k Keeper) callContract(ctx sdk.Context, clientID string, clientStore storetypes.KVStore, checksum types.Checksum, msg []byte) (*wasmvmtypes.ContractResult, error) { sdkGasMeter := ctx.GasMeter() - multipliedGasMeter := NewMultipliedGasMeter(sdkGasMeter, VMGasRegister) + multipliedGasMeter := types.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.SetupContractCost(true, len(msg)), "Loading CosmWasm module: sudo") - resp, gasUsed, err := ibcwasm.GetVM().Sudo(checksum, env, msg, newStoreAdapter(clientStore), wasmvmAPI, newQueryHandler(ctx, clientID), multipliedGasMeter, gasLimit, costJSONDeserialization) + resp, gasUsed, err := k.GetVM().Sudo(checksum, env, msg, types.NewStoreAdapter(clientStore), wasmvmAPI, k.newQueryHandler(ctx, clientID), multipliedGasMeter, gasLimit, types.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.ContractResult, error) { +// queryContract calls vm.Query. +func (k Keeper) queryContract(ctx sdk.Context, clientID string, clientStore storetypes.KVStore, checksum types.Checksum, msg []byte) (*wasmvmtypes.QueryResult, error) { sdkGasMeter := ctx.GasMeter() - multipliedGasMeter := NewMultipliedGasMeter(sdkGasMeter, VMGasRegister) + multipliedGasMeter := types.NewMultipliedGasMeter(sdkGasMeter, VMGasRegister) gasLimit := VMGasRegister.RuntimeGasForContract(ctx) env := getEnv(ctx, clientID) - ctx.GasMeter().ConsumeGas(VMGasRegister.SetupContractCost(true, len(msg)), "Loading CosmWasm module: migrate") - resp, gasUsed, err := ibcwasm.GetVM().Migrate(checksum, env, msg, newStoreAdapter(clientStore), wasmvmAPI, newQueryHandler(ctx, clientID), multipliedGasMeter, gasLimit, costJSONDeserialization) + ctx.GasMeter().ConsumeGas(VMGasRegister.SetupContractCost(true, len(msg)), "Loading CosmWasm module: query") + resp, gasUsed, err := k.GetVM().Query(checksum, env, msg, types.NewStoreAdapter(clientStore), wasmvmAPI, k.newQueryHandler(ctx, clientID), multipliedGasMeter, gasLimit, types.CostJSONDeserialization) VMGasRegister.ConsumeRuntimeGas(ctx, gasUsed) + return resp, err } -// queryContract calls vm.Query. -func queryContract(ctx sdk.Context, clientStore storetypes.KVStore, checksum Checksum, msg []byte) (*wasmvmtypes.QueryResult, error) { +// migrateContract calls vm.Migrate with internally constructed gas meter and environment. +func (k Keeper) migrateContract(ctx sdk.Context, clientID string, clientStore storetypes.KVStore, checksum types.Checksum, msg []byte) (*wasmvmtypes.ContractResult, error) { sdkGasMeter := ctx.GasMeter() - multipliedGasMeter := NewMultipliedGasMeter(sdkGasMeter, VMGasRegister) + multipliedGasMeter := types.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.SetupContractCost(true, len(msg)), "Loading CosmWasm module: query") - resp, gasUsed, err := ibcwasm.GetVM().Query(checksum, env, msg, newStoreAdapter(clientStore), wasmvmAPI, newQueryHandler(ctx, clientID), multipliedGasMeter, gasLimit, costJSONDeserialization) + ctx.GasMeter().ConsumeGas(VMGasRegister.SetupContractCost(true, len(msg)), "Loading CosmWasm module: migrate") + resp, gasUsed, err := k.GetVM().Migrate(checksum, env, msg, types.NewStoreAdapter(clientStore), wasmvmAPI, k.newQueryHandler(ctx, clientID), multipliedGasMeter, gasLimit, types.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 { +// WasmInstantiate accepts a message to instantiate a wasm contract, JSON encodes it and calls instantiateContract. +func (k Keeper) WasmInstantiate(ctx sdk.Context, clientID string, clientStore storetypes.KVStore, cs *types.ClientState, payload types.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 - res, err := instantiateContract(ctx, clientStore, checksum, encodedData) + res, err := k.instantiateContract(ctx, clientID, clientStore, checksum, encodedData) if err != nil { - return errorsmod.Wrap(ErrVMError, err.Error()) + return errorsmod.Wrap(types.ErrVMError, err.Error()) } if res.Err != "" { - return errorsmod.Wrap(ErrWasmContractCallFailed, res.Err) + return errorsmod.Wrap(types.ErrWasmContractCallFailed, res.Err) } if err = checkResponse(res.Ok); err != nil { return errorsmod.Wrapf(err, "checksum (%s)", hex.EncodeToString(cs.Checksum)) } - newClientState, err := validatePostExecutionClientState(clientStore, cdc) + newClientState, err := validatePostExecutionClientState(clientStore, k.Codec()) 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 errorsmod.Wrapf(types.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 +// WasmSudo calls the contract with the given payload and returns the result. +// WasmSudo returns an error if: // - 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 - +func (k Keeper) WasmSudo(ctx sdk.Context, clientID string, clientStore storetypes.KVStore, cs *types.ClientState, payload types.SudoMsg) ([]byte, error) { encodedData, err := json.Marshal(payload) if err != nil { - return result, errorsmod.Wrap(err, "failed to marshal payload for wasm execution") + return nil, errorsmod.Wrap(err, "failed to marshal payload for wasm execution") } checksum := cs.Checksum - res, err := callContract(ctx, clientStore, checksum, encodedData) + res, err := k.callContract(ctx, clientID, clientStore, checksum, encodedData) if err != nil { - return result, errorsmod.Wrap(ErrVMError, err.Error()) + return nil, errorsmod.Wrap(types.ErrVMError, err.Error()) } if res.Err != "" { - return result, errorsmod.Wrap(ErrWasmContractCallFailed, res.Err) + return nil, errorsmod.Wrap(types.ErrWasmContractCallFailed, res.Err) } if err = checkResponse(res.Ok); err != nil { - return result, errorsmod.Wrapf(err, "checksum (%s)", hex.EncodeToString(cs.Checksum)) + return nil, errorsmod.Wrapf(err, "checksum (%s)", hex.EncodeToString(cs.Checksum)) } - if err := json.Unmarshal(res.Ok.Data, &result); err != nil { - return result, errorsmod.Wrap(ErrWasmInvalidResponseData, err.Error()) - } - - newClientState, err := validatePostExecutionClientState(clientStore, cdc) + newClientState, err := validatePostExecutionClientState(clientStore, k.Codec()) if err != nil { - return result, err + return nil, 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 nil, errorsmod.Wrapf(types.ErrWasmInvalidContractModification, "expected checksum %s, got %s", hex.EncodeToString(checksum), hex.EncodeToString(newClientState.Checksum)) } - return result, nil + return res.Ok.Data, nil } -// wasmMigrate migrate calls the migrate entry point of the contract with the given payload and returns the result. -// wasmMigrate returns an error if: +// 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 { - res, err := migrateContract(ctx, clientID, clientStore, cs.Checksum, payload) +func (k Keeper) WasmMigrate(ctx sdk.Context, clientStore storetypes.KVStore, cs *types.ClientState, clientID string, payload []byte) error { + res, err := k.migrateContract(ctx, clientID, clientStore, cs.Checksum, payload) if err != nil { - return errorsmod.Wrap(ErrVMError, err.Error()) + return errorsmod.Wrap(types.ErrVMError, err.Error()) } if res.Err != "" { - return errorsmod.Wrap(ErrWasmContractCallFailed, res.Err) + return errorsmod.Wrap(types.ErrWasmContractCallFailed, res.Err) } if err = checkResponse(res.Ok); err != nil { return errorsmod.Wrapf(err, "checksum (%s)", hex.EncodeToString(cs.Checksum)) } - _, err = validatePostExecutionClientState(clientStore, cdc) + _, err = validatePostExecutionClientState(clientStore, k.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 +// WasmQuery queries the contract with the given payload and returns the result. +// WasmQuery returns an error if: // - 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 - +func (k Keeper) WasmQuery(ctx sdk.Context, clientID string, clientStore storetypes.KVStore, cs *types.ClientState, payload types.QueryMsg) ([]byte, error) { encodedData, err := json.Marshal(payload) if err != nil { - return result, errorsmod.Wrap(err, "failed to marshal payload for wasm query") + return nil, errorsmod.Wrap(err, "failed to marshal payload for wasm query") } - res, err := queryContract(ctx, clientStore, cs.Checksum, encodedData) + res, err := k.queryContract(ctx, clientID, clientStore, cs.Checksum, encodedData) if err != nil { - return result, errorsmod.Wrap(ErrVMError, err.Error()) + return nil, errorsmod.Wrap(types.ErrVMError, err.Error()) } if res.Err != "" { - return result, errorsmod.Wrap(ErrWasmContractCallFailed, res.Err) - } - - if err := json.Unmarshal(res.Ok, &result); err != nil { - return result, errorsmod.Wrapf(ErrWasmInvalidResponseData, "failed to unmarshal result of wasm query: %v", err) + return nil, errorsmod.Wrap(types.ErrWasmContractCallFailed, res.Err) } - return result, nil + return res.Ok, nil } // validatePostExecutionClientState validates that the contract has not many any invalid modifications @@ -237,26 +213,26 @@ func wasmQuery[T ContractResult](ctx sdk.Context, clientStore storetypes.KVStore // - 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) { +func validatePostExecutionClientState(clientStore storetypes.KVStore, cdc codec.BinaryCodec) (*types.ClientState, error) { key := host.ClientStateKey() - _, ok := clientStore.(migrateClientWrappedStore) + _, ok := clientStore.(types.MigrateClientWrappedStore) if ok { - key = append(subjectPrefix, key...) + key = append(types.SubjectPrefix, key...) } bz := clientStore.Get(key) if len(bz) == 0 { - return nil, errorsmod.Wrap(ErrWasmInvalidContractModification, types.ErrClientNotFound.Error()) + return nil, errorsmod.Wrap(types.ErrWasmInvalidContractModification, clienttypes.ErrClientNotFound.Error()) } clientState, err := unmarshalClientState(cdc, bz) if err != nil { - return nil, errorsmod.Wrap(ErrWasmInvalidContractModification, err.Error()) + return nil, errorsmod.Wrap(types.ErrWasmInvalidContractModification, err.Error()) } - cs, ok := clientState.(*ClientState) + cs, ok := clientState.(*types.ClientState) if !ok { - return nil, errorsmod.Wrapf(ErrWasmInvalidContractModification, "expected client state type %T, got %T", (*ClientState)(nil), clientState) + return nil, errorsmod.Wrapf(types.ErrWasmInvalidContractModification, "expected client state type %T, got %T", (*types.ClientState)(nil), clientState) } return cs, nil @@ -317,13 +293,13 @@ func validateAddress(human string) (uint64, error) { 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 + return types.ErrWasmSubMessagesNotAllowed } if len(response.Events) > 0 { - return ErrWasmEventsNotAllowed + return types.ErrWasmEventsNotAllowed } if len(response.Attributes) > 0 { - return ErrWasmAttributesNotAllowed + return types.ErrWasmAttributesNotAllowed } return nil diff --git a/modules/light-clients/08-wasm/types/vm_test.go b/modules/light-clients/08-wasm/keeper/contract_keeper_test.go similarity index 92% rename from modules/light-clients/08-wasm/types/vm_test.go rename to modules/light-clients/08-wasm/keeper/contract_keeper_test.go index a2d4a2d2b9e..0727842b1a8 100644 --- a/modules/light-clients/08-wasm/types/vm_test.go +++ b/modules/light-clients/08-wasm/keeper/contract_keeper_test.go @@ -1,4 +1,4 @@ -package types_test +package keeper_test import ( "encoding/json" @@ -15,7 +15,7 @@ import ( localhost "github.com/cosmos/ibc-go/v8/modules/light-clients/09-localhost" ) -func (suite *TypesTestSuite) TestWasmInstantiate() { +func (suite *KeeperTestSuite) TestWasmInstantiate() { testCases := []struct { name string malleate func() @@ -165,17 +165,19 @@ func (suite *TypesTestSuite) TestWasmInstantiate() { tc := tc suite.Run(tc.name, func() { suite.SetupWasmWithMockVM() + checksum := storeWasmCode(suite, wasmtesting.Code) tc.malleate() initMsg := types.InstantiateMessage{ ClientState: clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), wasmtesting.MockTendermitClientState), ConsensusState: clienttypes.MustMarshalConsensusState(suite.chainA.App.AppCodec(), wasmtesting.MockTendermintClientConsensusState), - Checksum: suite.checksum, + Checksum: checksum, } clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), defaultWasmClientID) - err := types.WasmInstantiate(suite.chainA.GetContext(), suite.chainA.App.AppCodec(), clientStore, &types.ClientState{Checksum: suite.checksum}, initMsg) + wasmClientKeeper := GetSimApp(suite.chainA).WasmClientKeeper + err := wasmClientKeeper.WasmInstantiate(suite.chainA.GetContext(), defaultWasmClientID, clientStore, &types.ClientState{Checksum: checksum}, initMsg) expPass := tc.expError == nil if expPass { @@ -187,7 +189,7 @@ func (suite *TypesTestSuite) TestWasmInstantiate() { } } -func (suite *TypesTestSuite) TestWasmMigrate() { +func (suite *KeeperTestSuite) TestWasmMigrate() { testCases := []struct { name string malleate func() @@ -305,6 +307,7 @@ func (suite *TypesTestSuite) TestWasmMigrate() { tc := tc suite.Run(tc.name, func() { suite.SetupWasmWithMockVM() + _ = storeWasmCode(suite, wasmtesting.Code) endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) err := endpoint.CreateClient() @@ -313,7 +316,8 @@ func (suite *TypesTestSuite) TestWasmMigrate() { tc.malleate() clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), defaultWasmClientID) - err = types.WasmMigrate(suite.chainA.GetContext(), suite.chainA.App.AppCodec(), clientStore, &types.ClientState{}, defaultWasmClientID, []byte("{}")) + wasmClientKeeper := GetSimApp(suite.chainA).WasmClientKeeper + err = wasmClientKeeper.WasmMigrate(suite.chainA.GetContext(), clientStore, &types.ClientState{}, defaultWasmClientID, []byte("{}")) expPass := tc.expError == nil if expPass { @@ -325,7 +329,7 @@ func (suite *TypesTestSuite) TestWasmMigrate() { } } -func (suite *TypesTestSuite) TestWasmQuery() { +func (suite *KeeperTestSuite) TestWasmQuery() { var payload types.QueryMsg testCases := []struct { @@ -368,21 +372,13 @@ func (suite *TypesTestSuite) TestWasmQuery() { }, types.ErrWasmContractCallFailed, }, - { - "failure: response fails to unmarshal", - func() { - suite.mockVM.RegisterQueryCallback(types.StatusMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { - return &wasmvmtypes.QueryResult{Ok: []byte("invalid json")}, wasmtesting.DefaultGasUsed, nil - }) - }, - types.ErrWasmInvalidResponseData, - }, } for _, tc := range testCases { tc := tc suite.Run(tc.name, func() { suite.SetupWasmWithMockVM() + _ = storeWasmCode(suite, wasmtesting.Code) endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) err := endpoint.CreateClient() @@ -398,7 +394,8 @@ func (suite *TypesTestSuite) TestWasmQuery() { tc.malleate() - res, err := types.WasmQuery[types.StatusResult](suite.chainA.GetContext(), clientStore, wasmClientState, payload) + wasmClientKeeper := GetSimApp(suite.chainA).WasmClientKeeper + res, err := wasmClientKeeper.WasmQuery(suite.chainA.GetContext(), endpoint.ClientID, clientStore, wasmClientState, payload) expPass := tc.expError == nil if expPass { @@ -411,7 +408,7 @@ func (suite *TypesTestSuite) TestWasmQuery() { } } -func (suite *TypesTestSuite) TestWasmSudo() { +func (suite *KeeperTestSuite) TestWasmSudo() { var payload types.SudoMsg testCases := []struct { @@ -487,15 +484,6 @@ func (suite *TypesTestSuite) TestWasmSudo() { }, types.ErrWasmAttributesNotAllowed, }, - { - "failure: response fails to unmarshal", - func() { - suite.mockVM.RegisterSudoCallback(types.UpdateStateMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { - return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: []byte("invalid json")}}, wasmtesting.DefaultGasUsed, nil - }) - }, - types.ErrWasmInvalidResponseData, - }, { "failure: invalid clientstate type", func() { @@ -561,6 +549,7 @@ func (suite *TypesTestSuite) TestWasmSudo() { tc := tc suite.Run(tc.name, func() { suite.SetupWasmWithMockVM() + _ = storeWasmCode(suite, wasmtesting.Code) endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) err := endpoint.CreateClient() @@ -576,7 +565,8 @@ func (suite *TypesTestSuite) TestWasmSudo() { tc.malleate() - res, err := types.WasmSudo[types.UpdateStateResult](suite.chainA.GetContext(), suite.chainA.App.AppCodec(), clientStore, wasmClientState, payload) + wasmClientKeeper := GetSimApp(suite.chainA).WasmClientKeeper + res, err := wasmClientKeeper.WasmSudo(suite.chainA.GetContext(), endpoint.ClientID, clientStore, wasmClientState, payload) expPass := tc.expError == nil if expPass { diff --git a/modules/light-clients/08-wasm/keeper/export_test.go b/modules/light-clients/08-wasm/keeper/export_test.go new file mode 100644 index 00000000000..a1e992b7f8c --- /dev/null +++ b/modules/light-clients/08-wasm/keeper/export_test.go @@ -0,0 +1,8 @@ +package keeper + +import sdk "github.com/cosmos/cosmos-sdk/types" + +// MigrateContractCode is a wrapper around k.migrateContractCode to allow the method to be directly called in tests. +func (k Keeper) MigrateContractCode(ctx sdk.Context, clientID string, newChecksum, migrateMsg []byte) error { + return k.migrateContractCode(ctx, clientID, newChecksum, migrateMsg) +} diff --git a/modules/light-clients/08-wasm/keeper/genesis.go b/modules/light-clients/08-wasm/keeper/genesis.go index d7c813c7086..53ada55069f 100644 --- a/modules/light-clients/08-wasm/keeper/genesis.go +++ b/modules/light-clients/08-wasm/keeper/genesis.go @@ -5,7 +5,6 @@ import ( 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" ) @@ -13,7 +12,7 @@ import ( // state. func (k Keeper) InitGenesis(ctx sdk.Context, gs types.GenesisState) error { storeFn := func(code wasmvm.WasmCode, _ uint64) (wasmvm.Checksum, uint64, error) { - checksum, err := ibcwasm.GetVM().StoreCodeUnchecked(code) + checksum, err := k.GetVM().StoreCodeUnchecked(code) return checksum, 0, err } @@ -28,8 +27,8 @@ func (k Keeper) InitGenesis(ctx sdk.Context, gs types.GenesisState) error { // ExportGenesis returns the 08-wasm module's exported genesis. This includes the code // for all contracts previously stored. -func (Keeper) ExportGenesis(ctx sdk.Context) types.GenesisState { - checksums, err := types.GetAllChecksums(ctx) +func (k Keeper) ExportGenesis(ctx sdk.Context) types.GenesisState { + checksums, err := k.GetAllChecksums(ctx) if err != nil { panic(err) } @@ -37,7 +36,7 @@ func (Keeper) ExportGenesis(ctx sdk.Context) types.GenesisState { // Grab code from wasmVM and add to genesis state. var genesisState types.GenesisState for _, checksum := range checksums { - code, err := ibcwasm.GetVM().GetCode(checksum) + code, err := k.GetVM().GetCode(checksum) if err != nil { panic(err) } diff --git a/modules/light-clients/08-wasm/keeper/genesis_test.go b/modules/light-clients/08-wasm/keeper/genesis_test.go index 0daa4acc739..8accae7476c 100644 --- a/modules/light-clients/08-wasm/keeper/genesis_test.go +++ b/modules/light-clients/08-wasm/keeper/genesis_test.go @@ -56,7 +56,7 @@ func (suite *KeeperTestSuite) TestInitGenesis() { suite.Require().NoError(err) var storedHashes []string - checksums, err := types.GetAllChecksums(suite.chainA.GetContext()) + checksums, err := GetSimApp(suite.chainA).WasmClientKeeper.GetAllChecksums(suite.chainA.GetContext()) suite.Require().NoError(err) for _, hash := range checksums { diff --git a/modules/light-clients/08-wasm/keeper/grpc_query.go b/modules/light-clients/08-wasm/keeper/grpc_query.go index 44216fd2454..21b2a558894 100644 --- a/modules/light-clients/08-wasm/keeper/grpc_query.go +++ b/modules/light-clients/08-wasm/keeper/grpc_query.go @@ -13,14 +13,13 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkquery "github.com/cosmos/cosmos-sdk/types/query" - "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" ) var _ types.QueryServer = (*Keeper)(nil) // Code implements the Query/Code gRPC method -func (Keeper) Code(goCtx context.Context, req *types.QueryCodeRequest) (*types.QueryCodeResponse, error) { +func (k Keeper) Code(goCtx context.Context, req *types.QueryCodeRequest) (*types.QueryCodeResponse, error) { if req == nil { return nil, status.Error(codes.InvalidArgument, "empty request") } @@ -31,11 +30,11 @@ func (Keeper) Code(goCtx context.Context, req *types.QueryCodeRequest) (*types.Q } // Only return checksums we previously stored, not arbitrary checksums that might be stored via e.g Wasmd. - if !types.HasChecksum(sdk.UnwrapSDKContext(goCtx), checksum) { + if !k.HasChecksum(sdk.UnwrapSDKContext(goCtx), checksum) { return nil, status.Error(codes.NotFound, errorsmod.Wrap(types.ErrWasmChecksumNotFound, req.Checksum).Error()) } - code, err := ibcwasm.GetVM().GetCode(checksum) + code, err := k.GetVM().GetCode(checksum) if err != nil { return nil, status.Error(codes.NotFound, errorsmod.Wrap(types.ErrWasmChecksumNotFound, req.Checksum).Error()) } @@ -46,10 +45,10 @@ func (Keeper) Code(goCtx context.Context, req *types.QueryCodeRequest) (*types.Q } // Checksums implements the Query/Checksums gRPC method. It returns a list of hex encoded checksums stored. -func (Keeper) Checksums(goCtx context.Context, req *types.QueryChecksumsRequest) (*types.QueryChecksumsResponse, error) { +func (k Keeper) Checksums(goCtx context.Context, req *types.QueryChecksumsRequest) (*types.QueryChecksumsResponse, error) { checksums, pageRes, err := sdkquery.CollectionPaginate( goCtx, - ibcwasm.Checksums, + k.GetChecksums(), req.Pagination, func(key []byte, value collections.NoValue) (string, error) { return hex.EncodeToString(key), nil diff --git a/modules/light-clients/08-wasm/keeper/keeper.go b/modules/light-clients/08-wasm/keeper/keeper.go index 9f323cf3a8f..70787b21386 100644 --- a/modules/light-clients/08-wasm/keeper/keeper.go +++ b/modules/light-clients/08-wasm/keeper/keeper.go @@ -2,12 +2,15 @@ package keeper import ( "bytes" + "context" "encoding/hex" wasmvm "github.com/CosmWasm/wasmvm/v2" + "cosmossdk.io/collections" "cosmossdk.io/core/store" errorsmod "cosmossdk.io/errors" + "cosmossdk.io/log" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" @@ -15,6 +18,7 @@ import ( "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" + "github.com/cosmos/ibc-go/v8/modules/core/exported" ) // Keeper defines the 08-wasm keeper @@ -22,11 +26,16 @@ type Keeper struct { // implements gRPC QueryServer interface types.QueryServer - cdc codec.BinaryCodec + cdc codec.BinaryCodec + clientKeeper types.ClientKeeper + + vm ibcwasm.WasmEngine + schema collections.Schema + checksums collections.KeySet[[]byte] storeService store.KVStoreService - clientKeeper types.ClientKeeper + queryPlugins QueryPlugins authority string } @@ -41,7 +50,40 @@ func (k Keeper) GetAuthority() string { return k.authority } -func (Keeper) storeWasmCode(ctx sdk.Context, code []byte, storeFn func(code wasmvm.WasmCode, gasLimit uint64) (wasmvm.Checksum, uint64, error)) ([]byte, error) { +// Logger returns a module-specific logger. +func (Keeper) Logger(ctx sdk.Context) log.Logger { + return moduleLogger(ctx) +} + +func moduleLogger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", "x/"+exported.ModuleName+"-"+types.ModuleName) +} + +// GetVM returns the keeper's vm engine. +func (k Keeper) GetVM() ibcwasm.WasmEngine { + return k.vm +} + +// GetChecksums returns the stored checksums. +func (k Keeper) GetChecksums() collections.KeySet[[]byte] { + return k.checksums +} + +// GetQueryPlugins returns the set query plugins. +func (k Keeper) GetQueryPlugins() QueryPlugins { + return k.queryPlugins +} + +// SetQueryPlugins sets the plugins. +func (k *Keeper) SetQueryPlugins(plugins QueryPlugins) { + k.queryPlugins = plugins +} + +func (k Keeper) newQueryHandler(ctx sdk.Context, callerID string) *queryHandler { + return newQueryHandler(ctx, k.GetQueryPlugins(), callerID) +} + +func (k Keeper) storeWasmCode(ctx sdk.Context, code []byte, storeFn func(code wasmvm.WasmCode, gasLimit uint64) (wasmvm.Checksum, uint64, error)) ([]byte, error) { var err error if types.IsGzip(code) { ctx.GasMeter().ConsumeGas(types.VMGasRegister.UncompressCosts(len(code)), "Uncompress gzip bytecode") @@ -62,7 +104,7 @@ func (Keeper) storeWasmCode(ctx sdk.Context, code []byte, storeFn func(code wasm return nil, errorsmod.Wrap(err, "wasm bytecode checksum failed") } - if types.HasChecksum(ctx, checksum) { + if k.HasChecksum(ctx, checksum) { return nil, types.ErrWasmCodeExists } @@ -80,12 +122,12 @@ func (Keeper) storeWasmCode(ctx sdk.Context, code []byte, storeFn func(code wasm } // pin the code to the vm in-memory cache - if err := ibcwasm.GetVM().Pin(vmChecksum); err != nil { + if err := k.GetVM().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) + err = k.GetChecksums().Set(ctx, checksum) if err != nil { return nil, errorsmod.Wrap(err, "failed to store checksum") } @@ -101,10 +143,27 @@ func (k Keeper) migrateContractCode(ctx sdk.Context, clientID string, newChecksu oldChecksum := wasmClientState.Checksum clientStore := k.clientKeeper.ClientStore(ctx, clientID) + clientState, found := types.GetClientState(clientStore, k.cdc) + if !found { + return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) + } + + if !k.HasChecksum(ctx, newChecksum) { + return types.ErrWasmChecksumNotFound + } + + if bytes.Equal(clientState.Checksum, newChecksum) { + return errorsmod.Wrapf(types.ErrWasmCodeExists, "new checksum (%s) is the same as current checksum (%s)", hex.EncodeToString(newChecksum), hex.EncodeToString(clientState.Checksum)) + } - err = wasmClientState.MigrateContract(ctx, k.cdc, clientStore, clientID, newChecksum, migrateMsg) + // update the checksum, this needs to be done before the contract migration + // so that wasmMigrate can call the right code. Note that this is not + // persisted to the client store. + clientState.Checksum = newChecksum + + err = k.WasmMigrate(ctx, clientStore, clientState, clientID, migrateMsg) if err != nil { - return errorsmod.Wrap(err, "contract migration failed") + return err } // client state may be updated by the contract migration @@ -140,15 +199,47 @@ func (k Keeper) GetWasmClientState(ctx sdk.Context, clientID string) (*types.Cli return wasmClientState, nil } +// GetAllChecksums is a helper to get all checksums from the store. +// It returns an empty slice if no checksums are found +func (k Keeper) GetAllChecksums(ctx context.Context) ([]types.Checksum, error) { + iterator, err := k.GetChecksums().Iterate(ctx, nil) + if err != nil { + return nil, err + } + + keys, err := iterator.Keys() + if err != nil { + return nil, err + } + + checksums := []types.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 (k Keeper) HasChecksum(ctx context.Context, checksum types.Checksum) bool { + found, err := k.GetChecksums().Has(ctx, checksum) + if err != nil { + return false + } + + return found +} + // InitializePinnedCodes updates wasmvm to pin to cache all contracts marked as pinned -func InitializePinnedCodes(ctx sdk.Context) error { - checksums, err := types.GetAllChecksums(ctx) +func (k Keeper) InitializePinnedCodes(ctx sdk.Context) error { + checksums, err := k.GetAllChecksums(ctx) if err != nil { return err } for _, checksum := range checksums { - if err := ibcwasm.GetVM().Pin(checksum); err != nil { + if err := k.GetVM().Pin(checksum); err != nil { return err } } diff --git a/modules/light-clients/08-wasm/keeper/keeper_test.go b/modules/light-clients/08-wasm/keeper/keeper_test.go index d1c0a4f3863..90f6d2b5223 100644 --- a/modules/light-clients/08-wasm/keeper/keeper_test.go +++ b/modules/light-clients/08-wasm/keeper/keeper_test.go @@ -19,7 +19,6 @@ import ( 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" "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/keeper" wasmtesting "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing" "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/testing/simapp" @@ -156,7 +155,7 @@ func (suite *KeeperTestSuite) TestNewKeeper() { runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), - ibcwasm.GetVM(), + GetSimApp(suite.chainA).WasmClientKeeper.GetVM(), GetSimApp(suite.chainA).GRPCQueryRouter(), ) }, @@ -171,7 +170,7 @@ func (suite *KeeperTestSuite) TestNewKeeper() { runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, "", // authority - ibcwasm.GetVM(), + GetSimApp(suite.chainA).WasmClientKeeper.GetVM(), GetSimApp(suite.chainA).GRPCQueryRouter(), ) }, @@ -186,7 +185,7 @@ func (suite *KeeperTestSuite) TestNewKeeper() { runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), nil, // client keeper, GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), - ibcwasm.GetVM(), + GetSimApp(suite.chainA).WasmClientKeeper.GetVM(), GetSimApp(suite.chainA).GRPCQueryRouter(), ) }, @@ -216,7 +215,7 @@ func (suite *KeeperTestSuite) TestNewKeeper() { nil, GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), - ibcwasm.GetVM(), + GetSimApp(suite.chainA).WasmClientKeeper.GetVM(), GetSimApp(suite.chainA).GRPCQueryRouter(), ) }, @@ -231,7 +230,7 @@ func (suite *KeeperTestSuite) TestNewKeeper() { runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), - ibcwasm.GetVM(), + GetSimApp(suite.chainA).WasmClientKeeper.GetVM(), nil, ) }, @@ -313,7 +312,7 @@ func (suite *KeeperTestSuite) TestInitializedPinnedCodes() { // malleate after storing contracts tc.malleate() - err := keeper.InitializePinnedCodes(ctx) + err := wasmClientKeeper.InitializePinnedCodes(ctx) expPass := tc.expError == nil if expPass { @@ -325,3 +324,242 @@ func (suite *KeeperTestSuite) TestInitializedPinnedCodes() { }) } } + +func (suite *KeeperTestSuite) TestMigrateContract() { + var ( + oldHash []byte + newHash []byte + payload []byte + expClientState *types.ClientState + ) + + testCases := []struct { + name string + malleate func() + expErr error + }{ + { + "success: update client state", + func() { + suite.mockVM.MigrateFn = func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + expClientState = types.NewClientState([]byte{1}, newHash, clienttypes.NewHeight(2000, 2)) + store.Set(host.ClientStateKey(), clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), expClientState)) + + data, err := json.Marshal(types.EmptyResult{}) + suite.Require().NoError(err) + + return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: data}}, wasmtesting.DefaultGasUsed, nil + } + }, + nil, + }, + { + "failure: new and old checksum are the same", + func() { + newHash = oldHash + // this should not be called + suite.mockVM.MigrateFn = func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + panic("unreachable") + } + }, + types.ErrWasmCodeExists, + }, + { + "failure: checksum not found", + func() { + err := GetSimApp(suite.chainA).WasmClientKeeper.GetChecksums().Remove(suite.chainA.GetContext(), newHash) + suite.Require().NoError(err) + }, + types.ErrWasmChecksumNotFound, + }, + { + "failure: vm returns error", + func() { + err := GetSimApp(suite.chainA).WasmClientKeeper.GetChecksums().Set(suite.chainA.GetContext(), newHash) + suite.Require().NoError(err) + + suite.mockVM.MigrateFn = func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + return nil, wasmtesting.DefaultGasUsed, wasmtesting.ErrMockVM + } + }, + types.ErrVMError, + }, + { + "failure: contract returns error", + func() { + err := GetSimApp(suite.chainA).WasmClientKeeper.GetChecksums().Set(suite.chainA.GetContext(), newHash) + suite.Require().NoError(err) + + suite.mockVM.MigrateFn = func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + return &wasmvmtypes.ContractResult{Err: wasmtesting.ErrMockContract.Error()}, wasmtesting.DefaultGasUsed, nil + } + }, + types.ErrWasmContractCallFailed, + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() + storeWasmCode(suite, wasmtesting.Code) + + var err error + oldHash, err = types.CreateChecksum(wasmtesting.Code) + suite.Require().NoError(err) + newHash, err = types.CreateChecksum(wasmtesting.CreateMockContract([]byte{1, 2, 3})) + suite.Require().NoError(err) + + wasmClientKeeper := GetSimApp(suite.chainA).WasmClientKeeper + err = wasmClientKeeper.GetChecksums().Set(suite.chainA.GetContext(), newHash) + suite.Require().NoError(err) + + endpointA := wasmtesting.NewWasmEndpoint(suite.chainA) + err = endpointA.CreateClient() + suite.Require().NoError(err) + + clientState, ok := endpointA.GetClientState().(*types.ClientState) + suite.Require().True(ok) + + expClientState = clientState + + tc.malleate() + + err = wasmClientKeeper.MigrateContractCode(suite.chainA.GetContext(), endpointA.ClientID, newHash, payload) + + // updated client state + clientState, ok = endpointA.GetClientState().(*types.ClientState) + suite.Require().True(ok) + + expPass := tc.expErr == nil + if expPass { + suite.Require().NoError(err) + suite.Require().Equal(expClientState, clientState) + } else { + suite.Require().ErrorIs(err, tc.expErr) + } + }) + } +} + +func (suite *KeeperTestSuite) TestGetChecksums() { + testCases := []struct { + name string + malleate func() + expResult func(checksums []types.Checksum) + }{ + { + "success: no contract stored.", + func() {}, + func(checksums []types.Checksum) { + suite.Require().Len(checksums, 0) + }, + }, + { + "success: default mock vm contract stored.", + func() { + suite.SetupWasmWithMockVM() + storeWasmCode(suite, wasmtesting.Code) + }, + func(checksums []types.Checksum) { + suite.Require().Len(checksums, 1) + expectedChecksum, err := types.CreateChecksum(wasmtesting.Code) + suite.Require().NoError(err) + suite.Require().Equal(expectedChecksum, checksums[0]) + }, + }, + { + "success: non-empty checksums", + func() { + suite.SetupWasmWithMockVM() + storeWasmCode(suite, wasmtesting.Code) + + err := GetSimApp(suite.chainA).WasmClientKeeper.GetChecksums().Set(suite.chainA.GetContext(), types.Checksum("checksum")) + suite.Require().NoError(err) + }, + func(checksums []types.Checksum) { + suite.Require().Len(checksums, 2) + suite.Require().Contains(checksums, types.Checksum("checksum")) + }, + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + tc.malleate() + + checksums, err := GetSimApp(suite.chainA).WasmClientKeeper.GetAllChecksums(suite.chainA.GetContext()) + suite.Require().NoError(err) + tc.expResult(checksums) + }) + } +} + +func (suite *KeeperTestSuite) TestAddChecksum() { + suite.SetupWasmWithMockVM() + storeWasmCode(suite, wasmtesting.Code) + + wasmClientKeeper := GetSimApp(suite.chainA).WasmClientKeeper + + checksums, err := wasmClientKeeper.GetAllChecksums(suite.chainA.GetContext()) + suite.Require().NoError(err) + // default mock vm contract is stored + suite.Require().Len(checksums, 1) + + checksum1 := types.Checksum("checksum1") + checksum2 := types.Checksum("checksum2") + err = wasmClientKeeper.GetChecksums().Set(suite.chainA.GetContext(), checksum1) + suite.Require().NoError(err) + err = wasmClientKeeper.GetChecksums().Set(suite.chainA.GetContext(), checksum2) + suite.Require().NoError(err) + + // Test adding the same checksum twice + err = wasmClientKeeper.GetChecksums().Set(suite.chainA.GetContext(), checksum1) + suite.Require().NoError(err) + + checksums, err = wasmClientKeeper.GetAllChecksums(suite.chainA.GetContext()) + suite.Require().NoError(err) + suite.Require().Len(checksums, 3) + suite.Require().Contains(checksums, checksum1) + suite.Require().Contains(checksums, checksum2) +} + +func (suite *KeeperTestSuite) TestHasChecksum() { + var checksum types.Checksum + + testCases := []struct { + name string + malleate func() + exprResult bool + }{ + { + "success: checksum exists", + func() { + checksum = types.Checksum("checksum") + err := GetSimApp(suite.chainA).WasmClientKeeper.GetChecksums().Set(suite.chainA.GetContext(), checksum) + suite.Require().NoError(err) + }, + true, + }, + { + "success: checksum does not exist", + func() { + checksum = types.Checksum("non-existent-checksum") + }, + false, + }, + } + + for _, tc := range testCases { + tc := tc + suite.Run(tc.name, func() { + suite.SetupWasmWithMockVM() + + tc.malleate() + + result := GetSimApp(suite.chainA).WasmClientKeeper.HasChecksum(suite.chainA.GetContext(), checksum) + suite.Require().Equal(tc.exprResult, result) + }) + } +} diff --git a/modules/light-clients/08-wasm/keeper/keeper_vm.go b/modules/light-clients/08-wasm/keeper/keeper_vm.go index 6173ceb06b8..16829c24b64 100644 --- a/modules/light-clients/08-wasm/keeper/keeper_vm.go +++ b/modules/light-clients/08-wasm/keeper/keeper_vm.go @@ -9,6 +9,7 @@ import ( wasmvm "github.com/CosmWasm/wasmvm/v2" + "cosmossdk.io/collections" "cosmossdk.io/core/store" "github.com/cosmos/cosmos-sdk/codec" @@ -33,6 +34,10 @@ func NewKeeperWithVM( panic(errors.New("client keeper must not be nil")) } + if queryRouter == nil { + panic(errors.New("query router must not be nil")) + } + if vm == nil { panic(errors.New("wasm VM must not be nil")) } @@ -45,24 +50,33 @@ func NewKeeperWithVM( panic(errors.New("authority must be non-empty")) } + sb := collections.NewSchemaBuilder(storeService) + keeper := &Keeper{ cdc: cdc, + vm: vm, + checksums: collections.NewKeySet(sb, types.ChecksumsKey, "checksums", collections.BytesKey), storeService: storeService, clientKeeper: clientKeeper, authority: authority, } + schema, err := sb.Build() + if err != nil { + panic(err) + } + + // TODO(jim): Is this needed? wasmd doesn't hold ref, sdk docs/code do. + keeper.schema = schema + // set query plugins to ensure there is a non-nil query plugin // regardless of what options the user provides - ibcwasm.SetQueryPlugins(types.NewDefaultQueryPlugins()) + keeper.SetQueryPlugins(NewDefaultQueryPlugins(queryRouter)) + for _, opt := range opts { opt.apply(keeper) } - ibcwasm.SetVM(vm) - ibcwasm.SetQueryRouter(queryRouter) - ibcwasm.SetupWasmStoreService(storeService) - return *keeper } diff --git a/modules/light-clients/08-wasm/keeper/migrations.go b/modules/light-clients/08-wasm/keeper/migrations.go index 9961a7251a0..bf0a1488494 100644 --- a/modules/light-clients/08-wasm/keeper/migrations.go +++ b/modules/light-clients/08-wasm/keeper/migrations.go @@ -3,7 +3,6 @@ package keeper import ( 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" ) @@ -32,7 +31,7 @@ func (m Migrator) MigrateChecksums(ctx sdk.Context) error { } for _, hash := range checksums { - if err := ibcwasm.Checksums.Set(ctx, hash); err != nil { + if err := m.keeper.GetChecksums().Set(ctx, hash); err != nil { return err } } @@ -42,7 +41,7 @@ func (m Migrator) MigrateChecksums(ctx sdk.Context) error { return err } - types.Logger(ctx).Info("successfully migrated Checksums to collections") + m.keeper.Logger(ctx).Info("successfully migrated Checksums to collections") return nil } diff --git a/modules/light-clients/08-wasm/keeper/migrations_test.go b/modules/light-clients/08-wasm/keeper/migrations_test.go index 39c2d6ab24b..8735b6bb11e 100644 --- a/modules/light-clients/08-wasm/keeper/migrations_test.go +++ b/modules/light-clients/08-wasm/keeper/migrations_test.go @@ -1,7 +1,6 @@ package keeper_test import ( - "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/keeper" "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" ) @@ -29,15 +28,15 @@ func (suite *KeeperTestSuite) TestMigrateWasmStore() { suite.storeChecksums(tc.checksums) // run the migration - wasmKeeper := GetSimApp(suite.chainA).WasmClientKeeper - m := keeper.NewMigrator(wasmKeeper) + wasmClientKeeper := GetSimApp(suite.chainA).WasmClientKeeper + m := keeper.NewMigrator(wasmClientKeeper) err := m.MigrateChecksums(suite.chainA.GetContext()) suite.Require().NoError(err) // check that they were stored in KeySet for _, hash := range tc.checksums { - suite.Require().True(ibcwasm.Checksums.Has(suite.chainA.GetContext(), hash)) + suite.Require().True(wasmClientKeeper.GetChecksums().Has(suite.chainA.GetContext(), hash)) } // check that the data under the old key was deleted diff --git a/modules/light-clients/08-wasm/keeper/msg_server.go b/modules/light-clients/08-wasm/keeper/msg_server.go index 46d55f397b5..606baaa9fad 100644 --- a/modules/light-clients/08-wasm/keeper/msg_server.go +++ b/modules/light-clients/08-wasm/keeper/msg_server.go @@ -8,7 +8,6 @@ import ( 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" ibcerrors "github.com/cosmos/ibc-go/v8/modules/core/errors" ) @@ -22,7 +21,7 @@ func (k Keeper) StoreCode(goCtx context.Context, msg *types.MsgStoreCode) (*type } ctx := sdk.UnwrapSDKContext(goCtx) - checksum, err := k.storeWasmCode(ctx, msg.WasmByteCode, ibcwasm.GetVM().StoreCode) + checksum, err := k.storeWasmCode(ctx, msg.WasmByteCode, k.GetVM().StoreCode) if err != nil { return nil, errorsmod.Wrap(err, "failed to store wasm bytecode") } @@ -40,17 +39,17 @@ func (k Keeper) RemoveChecksum(goCtx context.Context, msg *types.MsgRemoveChecks return nil, errorsmod.Wrapf(ibcerrors.ErrUnauthorized, "expected %s, got %s", k.GetAuthority(), msg.Signer) } - if !types.HasChecksum(goCtx, msg.Checksum) { + if !k.HasChecksum(goCtx, msg.Checksum) { return nil, types.ErrWasmChecksumNotFound } - err := ibcwasm.Checksums.Remove(goCtx, msg.Checksum) + err := k.GetChecksums().Remove(goCtx, msg.Checksum) if err != nil { return nil, errorsmod.Wrap(err, "failed to remove checksum") } // unpin the code from the vm in-memory cache - if err := ibcwasm.GetVM().Unpin(msg.Checksum); err != nil { + if err := k.GetVM().Unpin(msg.Checksum); err != nil { return nil, errorsmod.Wrapf(err, "failed to unpin contract with checksum (%s) from vm cache", hex.EncodeToString(msg.Checksum)) } diff --git a/modules/light-clients/08-wasm/keeper/msg_server_test.go b/modules/light-clients/08-wasm/keeper/msg_server_test.go index b3b67b1996e..6c4dfa59abc 100644 --- a/modules/light-clients/08-wasm/keeper/msg_server_test.go +++ b/modules/light-clients/08-wasm/keeper/msg_server_test.go @@ -12,7 +12,6 @@ import ( 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" @@ -358,7 +357,8 @@ func (suite *KeeperTestSuite) TestMsgRemoveChecksum() { checksum, err := types.CreateChecksum(mockCode) suite.Require().NoError(err) - err = ibcwasm.Checksums.Set(suite.chainA.GetContext(), checksum) + keeper := GetSimApp(suite.chainA).WasmClientKeeper + err = keeper.GetChecksums().Set(suite.chainA.GetContext(), checksum) suite.Require().NoError(err) expChecksums = append(expChecksums, checksum) @@ -413,7 +413,7 @@ func (suite *KeeperTestSuite) TestMsgRemoveChecksum() { suite.Require().NoError(err) suite.Require().NotNil(res) - checksums, err := types.GetAllChecksums(suite.chainA.GetContext()) + checksums, err := GetSimApp(suite.chainA).WasmClientKeeper.GetAllChecksums(suite.chainA.GetContext()) suite.Require().NoError(err) // Check equality of checksums up to order diff --git a/modules/light-clients/08-wasm/keeper/options.go b/modules/light-clients/08-wasm/keeper/options.go index 8977a6df371..320f37be351 100644 --- a/modules/light-clients/08-wasm/keeper/options.go +++ b/modules/light-clients/08-wasm/keeper/options.go @@ -1,12 +1,5 @@ package keeper -import ( - "errors" - - "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" - "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" -) - // Option is an extension point to instantiate keeper with non default values type Option interface { apply(*Keeper) @@ -20,13 +13,11 @@ func (f optsFn) apply(keeper *Keeper) { // WithQueryPlugins is an optional constructor parameter to pass custom query plugins for wasmVM requests. // Missing fields will be filled with default queriers. -func WithQueryPlugins(plugins *types.QueryPlugins) Option { - return optsFn(func(_ *Keeper) { - currentPlugins, ok := ibcwasm.GetQueryPlugins().(*types.QueryPlugins) - if !ok { - panic(errors.New("invalid query plugins type")) - } +func WithQueryPlugins(plugins *QueryPlugins) Option { + return optsFn(func(k *Keeper) { + currentPlugins := k.GetQueryPlugins() newPlugins := currentPlugins.Merge(plugins) - ibcwasm.SetQueryPlugins(&newPlugins) + + k.SetQueryPlugins(newPlugins) }) } diff --git a/modules/light-clients/08-wasm/keeper/options_test.go b/modules/light-clients/08-wasm/keeper/options_test.go index 61b686927ee..bec7233df21 100644 --- a/modules/light-clients/08-wasm/keeper/options_test.go +++ b/modules/light-clients/08-wasm/keeper/options_test.go @@ -9,18 +9,17 @@ import ( "github.com/cosmos/cosmos-sdk/runtime" 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/keeper" "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" ) -func MockCustomQuerier() func(sdk.Context, json.RawMessage) ([]byte, error) { +func mockErrorCustomQuerier() func(sdk.Context, json.RawMessage) ([]byte, error) { return func(_ sdk.Context, _ json.RawMessage) ([]byte, error) { return nil, errors.New("custom querier error for TestNewKeeperWithOptions") } } -func MockStargateQuerier() func(sdk.Context, *wasmvmtypes.StargateQuery) ([]byte, error) { +func mockErrorStargateQuerier() func(sdk.Context, *wasmvmtypes.StargateQuery) ([]byte, error) { return func(_ sdk.Context, _ *wasmvmtypes.StargateQuery) ([]byte, error) { return nil, errors.New("stargate querier error for TestNewKeeperWithOptions") } @@ -41,12 +40,12 @@ func (suite *KeeperTestSuite) TestNewKeeperWithOptions() { runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), - ibcwasm.GetVM(), + GetSimApp(suite.chainA).WasmClientKeeper.GetVM(), GetSimApp(suite.chainA).GRPCQueryRouter(), ) }, func(k keeper.Keeper) { - plugins := ibcwasm.GetQueryPlugins().(*types.QueryPlugins) + plugins := k.GetQueryPlugins() _, err := plugins.Custom(sdk.Context{}, nil) suite.Require().ErrorIs(err, wasmvmtypes.UnsupportedRequest{Kind: "Custom queries are not allowed"}) @@ -58,21 +57,21 @@ func (suite *KeeperTestSuite) TestNewKeeperWithOptions() { { "success: custom querier", func() { - querierOption := keeper.WithQueryPlugins(&types.QueryPlugins{ - Custom: MockCustomQuerier(), + querierOption := keeper.WithQueryPlugins(&keeper.QueryPlugins{ + Custom: mockErrorCustomQuerier(), }) k = keeper.NewKeeperWithVM( GetSimApp(suite.chainA).AppCodec(), runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), - ibcwasm.GetVM(), + GetSimApp(suite.chainA).WasmClientKeeper.GetVM(), GetSimApp(suite.chainA).GRPCQueryRouter(), querierOption, ) }, func(k keeper.Keeper) { - plugins := ibcwasm.GetQueryPlugins().(*types.QueryPlugins) + plugins := k.GetQueryPlugins() _, err := plugins.Custom(sdk.Context{}, nil) suite.Require().ErrorContains(err, "custom querier error for TestNewKeeperWithOptions") @@ -84,21 +83,21 @@ func (suite *KeeperTestSuite) TestNewKeeperWithOptions() { { "success: stargate querier", func() { - querierOption := keeper.WithQueryPlugins(&types.QueryPlugins{ - Stargate: MockStargateQuerier(), + querierOption := keeper.WithQueryPlugins(&keeper.QueryPlugins{ + Stargate: mockErrorStargateQuerier(), }) k = keeper.NewKeeperWithVM( GetSimApp(suite.chainA).AppCodec(), runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), - ibcwasm.GetVM(), + GetSimApp(suite.chainA).WasmClientKeeper.GetVM(), GetSimApp(suite.chainA).GRPCQueryRouter(), querierOption, ) }, func(k keeper.Keeper) { - plugins := ibcwasm.GetQueryPlugins().(*types.QueryPlugins) + plugins := k.GetQueryPlugins() _, err := plugins.Custom(sdk.Context{}, nil) suite.Require().ErrorIs(err, wasmvmtypes.UnsupportedRequest{Kind: "Custom queries are not allowed"}) @@ -110,22 +109,22 @@ func (suite *KeeperTestSuite) TestNewKeeperWithOptions() { { "success: both queriers", func() { - querierOption := keeper.WithQueryPlugins(&types.QueryPlugins{ - Custom: MockCustomQuerier(), - Stargate: MockStargateQuerier(), + querierOption := keeper.WithQueryPlugins(&keeper.QueryPlugins{ + Custom: mockErrorCustomQuerier(), + Stargate: mockErrorStargateQuerier(), }) k = keeper.NewKeeperWithVM( GetSimApp(suite.chainA).AppCodec(), runtime.NewKVStoreService(GetSimApp(suite.chainA).GetKey(types.StoreKey)), GetSimApp(suite.chainA).IBCKeeper.ClientKeeper, GetSimApp(suite.chainA).WasmClientKeeper.GetAuthority(), - ibcwasm.GetVM(), + GetSimApp(suite.chainA).WasmClientKeeper.GetVM(), GetSimApp(suite.chainA).GRPCQueryRouter(), querierOption, ) }, func(k keeper.Keeper) { - plugins := ibcwasm.GetQueryPlugins().(*types.QueryPlugins) + plugins := k.GetQueryPlugins() _, err := plugins.Custom(sdk.Context{}, nil) suite.Require().ErrorContains(err, "custom querier error for TestNewKeeperWithOptions") @@ -142,13 +141,13 @@ func (suite *KeeperTestSuite) TestNewKeeperWithOptions() { suite.Run(tc.name, func() { // make sure the default query plugins are set - ibcwasm.SetQueryPlugins(types.NewDefaultQueryPlugins()) + k.SetQueryPlugins(keeper.NewDefaultQueryPlugins(GetSimApp(suite.chainA).GRPCQueryRouter())) tc.malleate() tc.verifyFn(k) // reset query plugins after each test - ibcwasm.SetQueryPlugins(types.NewDefaultQueryPlugins()) + k.SetQueryPlugins(keeper.NewDefaultQueryPlugins(GetSimApp(suite.chainA).GRPCQueryRouter())) }) } } diff --git a/modules/light-clients/08-wasm/types/querier.go b/modules/light-clients/08-wasm/keeper/querier.go similarity index 87% rename from modules/light-clients/08-wasm/types/querier.go rename to modules/light-clients/08-wasm/keeper/querier.go index 5fbd7a57bf4..6d932ee57a0 100644 --- a/modules/light-clients/08-wasm/types/querier.go +++ b/modules/light-clients/08-wasm/keeper/querier.go @@ -1,4 +1,4 @@ -package types +package keeper import ( "encoding/json" @@ -29,10 +29,7 @@ to `baseapp.GRPCQueryRouter`. This design is based on wasmd's (v0.50.0) querier plugin design. */ -var ( - _ wasmvmtypes.Querier = (*queryHandler)(nil) - _ ibcwasm.QueryPluginsI = (*QueryPlugins)(nil) -) +var _ wasmvmtypes.Querier = (*queryHandler)(nil) // defaultAcceptList defines a set of default allowed queries made available to the Querier. var defaultAcceptList = []string{ @@ -43,13 +40,16 @@ var defaultAcceptList = []string{ // into the query plugins. type queryHandler struct { Ctx sdk.Context + Plugins QueryPlugins CallerID string } // newQueryHandler returns a default querier that can be used in the contract. -func newQueryHandler(ctx sdk.Context, callerID string) *queryHandler { +// TODO(jim): Make private and use export_test? +func newQueryHandler(ctx sdk.Context, plugins QueryPlugins, callerID string) *queryHandler { return &queryHandler{ Ctx: ctx, + Plugins: plugins, CallerID: callerID, } } @@ -71,12 +71,12 @@ func (q *queryHandler) Query(request wasmvmtypes.QueryRequest, gasLimit uint64) q.Ctx.GasMeter().ConsumeGas(subCtx.GasMeter().GasConsumed(), "contract sub-query") }() - res, err := ibcwasm.GetQueryPlugins().HandleQuery(subCtx, q.CallerID, request) + res, err := q.Plugins.HandleQuery(subCtx, q.CallerID, request) if err == nil { return res, nil } - Logger(q.Ctx).Debug("Redacting query error", "cause", err) + moduleLogger(q.Ctx).Debug("Redacting query error", "cause", err) return nil, redactError(err) } @@ -123,16 +123,16 @@ func (e QueryPlugins) HandleQuery(ctx sdk.Context, caller string, request wasmvm } // NewDefaultQueryPlugins returns the default set of query plugins -func NewDefaultQueryPlugins() *QueryPlugins { - return &QueryPlugins{ +func NewDefaultQueryPlugins(queryRouter ibcwasm.QueryRouter) QueryPlugins { + return QueryPlugins{ Custom: RejectCustomQuerier(), - Stargate: AcceptListStargateQuerier([]string{}), + Stargate: AcceptListStargateQuerier([]string{}, queryRouter), } } // AcceptListStargateQuerier allows all queries that are in the provided accept list. // This function returns protobuf encoded responses in bytes. -func AcceptListStargateQuerier(acceptedQueries []string) func(sdk.Context, *wasmvmtypes.StargateQuery) ([]byte, error) { +func AcceptListStargateQuerier(acceptedQueries []string, queryRouter ibcwasm.QueryRouter) func(sdk.Context, *wasmvmtypes.StargateQuery) ([]byte, error) { return func(ctx sdk.Context, request *wasmvmtypes.StargateQuery) ([]byte, error) { // append user defined accepted queries to default list defined above. acceptedQueries = append(defaultAcceptList, acceptedQueries...) @@ -142,7 +142,7 @@ func AcceptListStargateQuerier(acceptedQueries []string) func(sdk.Context, *wasm return nil, wasmvmtypes.UnsupportedRequest{Kind: fmt.Sprintf("'%s' path is not allowed from the contract", request.Path)} } - route := ibcwasm.GetQueryRouter().Route(request.Path) + route := queryRouter.Route(request.Path) if route == nil { return nil, wasmvmtypes.UnsupportedRequest{Kind: fmt.Sprintf("No route to query '%s'", request.Path)} } diff --git a/modules/light-clients/08-wasm/types/querier_test.go b/modules/light-clients/08-wasm/keeper/querier_test.go similarity index 80% rename from modules/light-clients/08-wasm/types/querier_test.go rename to modules/light-clients/08-wasm/keeper/querier_test.go index 7cc1b3d20a3..4af9074c12e 100644 --- a/modules/light-clients/08-wasm/types/querier_test.go +++ b/modules/light-clients/08-wasm/keeper/querier_test.go @@ -1,4 +1,4 @@ -package types_test +package keeper_test import ( "encoding/hex" @@ -11,7 +11,7 @@ import ( 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/keeper" 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" @@ -27,7 +27,7 @@ type QueryEcho struct { Data string `json:"data"` } -func MockCustomQuerier() func(sdk.Context, json.RawMessage) ([]byte, error) { +func mockCustomQuerier() func(sdk.Context, json.RawMessage) ([]byte, error) { return func(ctx sdk.Context, request json.RawMessage) ([]byte, error) { var customQuery CustomQuery err := json.Unmarshal([]byte(request), &customQuery) @@ -44,19 +44,20 @@ func MockCustomQuerier() func(sdk.Context, json.RawMessage) ([]byte, error) { } } -func (suite *TypesTestSuite) TestCustomQuery() { +func (suite *KeeperTestSuite) TestCustomQuery() { testCases := []struct { name string - malleate func() + malleate func(k *keeper.Keeper) + expError error }{ { "success: custom query", - func() { - querierPlugin := types.QueryPlugins{ - Custom: MockCustomQuerier(), + func(k *keeper.Keeper) { + querierPlugin := keeper.QueryPlugins{ + Custom: mockCustomQuerier(), } - ibcwasm.SetQueryPlugins(&querierPlugin) + k.SetQueryPlugins(querierPlugin) suite.mockVM.RegisterQueryCallback(types.StatusMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, querier wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { echo := CustomQuery{ Echo: &QueryEcho{ @@ -82,10 +83,11 @@ func (suite *TypesTestSuite) TestCustomQuery() { return &wasmvmtypes.QueryResult{Ok: resp}, wasmtesting.DefaultGasUsed, nil }) }, + nil, }, { "failure: default query", - func() { + func(k *keeper.Keeper) { suite.mockVM.RegisterQueryCallback(types.StatusMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, querier wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { resp, err := querier.Query(wasmvmtypes.QueryRequest{Custom: json.RawMessage("{}")}, math.MaxUint64) suite.Require().ErrorIs(err, wasmvmtypes.UnsupportedRequest{Kind: "Custom queries are not allowed"}) @@ -94,36 +96,50 @@ func (suite *TypesTestSuite) TestCustomQuery() { return nil, wasmtesting.DefaultGasUsed, err }) }, + types.ErrVMError, }, } for _, tc := range testCases { suite.Run(tc.name, func() { suite.SetupWasmWithMockVM() + _ = storeWasmCode(suite, wasmtesting.Code) endpoint := wasmtesting.NewWasmEndpoint(suite.chainA) err := endpoint.CreateClient() suite.Require().NoError(err) - tc.malleate() + wasmClientKeeper := GetSimApp(suite.chainA).WasmClientKeeper + + tc.malleate(&wasmClientKeeper) clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), endpoint.ClientID) clientState, ok := endpoint.GetClientState().(*types.ClientState) suite.Require().True(ok) - clientState.Status(suite.chainA.GetContext(), clientStore, suite.chainA.App.AppCodec()) + res, err := wasmClientKeeper.WasmQuery(suite.chainA.GetContext(), endpoint.ClientID, clientStore, clientState, types.QueryMsg{Status: &types.StatusMsg{}}) + + expPass := tc.expError == nil + if expPass { + suite.Require().Nil(err) + suite.Require().NotNil(res) + } else { + suite.Require().Nil(res) + suite.Require().ErrorIs(err, tc.expError) + } // reset query plugins after each test - ibcwasm.SetQueryPlugins(types.NewDefaultQueryPlugins()) + wasmClientKeeper.SetQueryPlugins(keeper.NewDefaultQueryPlugins(GetSimApp(suite.chainA).GRPCQueryRouter())) }) } } -func (suite *TypesTestSuite) TestStargateQuery() { +func (suite *KeeperTestSuite) TestStargateQuery() { typeURL := "/ibc.lightclients.wasm.v1.Query/Checksums" var ( endpoint *wasmtesting.WasmEndpoint + checksum []byte expDiscardedState = false proofKey = []byte("mock-key") testKey = []byte("test-key") @@ -132,17 +148,17 @@ func (suite *TypesTestSuite) TestStargateQuery() { testCases := []struct { name string - malleate func() + malleate func(k *keeper.Keeper) expError error }{ { "success: custom query", - func() { - querierPlugin := types.QueryPlugins{ - Stargate: types.AcceptListStargateQuerier([]string{typeURL}), + func(k *keeper.Keeper) { + querierPlugin := keeper.QueryPlugins{ + Stargate: keeper.AcceptListStargateQuerier([]string{typeURL}, GetSimApp(suite.chainA).GRPCQueryRouter()), } - ibcwasm.SetQueryPlugins(&querierPlugin) + k.SetQueryPlugins(querierPlugin) suite.mockVM.RegisterQueryCallback(types.TimestampAtHeightMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, querier wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { queryRequest := types.QueryChecksumsRequest{} @@ -161,7 +177,7 @@ func (suite *TypesTestSuite) TestStargateQuery() { err = respData.Unmarshal(resp) suite.Require().NoError(err) - expChecksum := hex.EncodeToString(suite.checksum) + expChecksum := hex.EncodeToString(checksum) suite.Require().Len(respData.Checksums, 1) suite.Require().Equal(expChecksum, respData.Checksums[0]) @@ -184,12 +200,12 @@ func (suite *TypesTestSuite) TestStargateQuery() { // This exercises the full flow through the grpc handler and into the light client for verification, handling encoding and routing. // Furthermore we write a test key and assert that the state changes made by this handler were discarded by the cachedCtx at the grpc handler. "success: verify membership query", - func() { - querierPlugin := types.QueryPlugins{ - Stargate: types.AcceptListStargateQuerier([]string{""}), + func(k *keeper.Keeper) { + querierPlugin := keeper.QueryPlugins{ + Stargate: keeper.AcceptListStargateQuerier([]string{""}, GetSimApp(suite.chainA).GRPCQueryRouter()), } - ibcwasm.SetQueryPlugins(&querierPlugin) + k.SetQueryPlugins(querierPlugin) store := suite.chainA.GetContext().KVStore(GetSimApp(suite.chainA).GetKey(exported.StoreKey)) store.Set(proofKey, value) @@ -261,7 +277,7 @@ func (suite *TypesTestSuite) TestStargateQuery() { }, { "failure: default querier", - func() { + func(k *keeper.Keeper) { suite.mockVM.RegisterQueryCallback(types.TimestampAtHeightMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, store wasmvm.KVStore, _ wasmvm.GoAPI, querier wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { queryRequest := types.QueryChecksumsRequest{} bz, err := queryRequest.Marshal() @@ -289,20 +305,31 @@ func (suite *TypesTestSuite) TestStargateQuery() { suite.Run(tc.name, func() { expDiscardedState = false suite.SetupWasmWithMockVM() + checksum = storeWasmCode(suite, wasmtesting.Code) endpoint = wasmtesting.NewWasmEndpoint(suite.chainA) err := endpoint.CreateClient() suite.Require().NoError(err) - tc.malleate() + wasmClientKeeper := GetSimApp(suite.chainA).WasmClientKeeper - clientModule, found := suite.chainA.App.GetIBCKeeper().ClientKeeper.Route(endpoint.ClientID) - suite.Require().True(found) + tc.malleate(&wasmClientKeeper) + + payload := types.QueryMsg{ + TimestampAtHeight: &types.TimestampAtHeightMsg{ + Height: clienttypes.NewHeight(1, 100), + }, + } + + clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), endpoint.ClientID) + clientState, ok := endpoint.GetClientState().(*types.ClientState) + suite.Require().True(ok) // NOTE: we register query callbacks against: types.TimestampAtHeightMsg{} // in practise, this can against any client state msg, however registering against types.StatusMsg{} introduces recursive loops // due to test case: "success: verify membership query" - _, err = clientModule.TimestampAtHeight(suite.chainA.GetContext(), endpoint.ClientID, clienttypes.NewHeight(1, 100)) + // TODO(Jim): Sanity check that it unmarshals correctly? + _, err = wasmClientKeeper.WasmQuery(suite.chainA.GetContext(), endpoint.ClientID, clientStore, clientState, payload) expPass := tc.expError == nil if expPass { @@ -312,7 +339,7 @@ func (suite *TypesTestSuite) TestStargateQuery() { suite.Require().ErrorContains(err, tc.expError.Error()) } - clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), endpoint.ClientID) + clientStore = suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), endpoint.ClientID) if expDiscardedState { suite.Require().False(clientStore.Has(testKey)) } else { @@ -320,7 +347,7 @@ func (suite *TypesTestSuite) TestStargateQuery() { } // reset query plugins after each test - ibcwasm.SetQueryPlugins(types.NewDefaultQueryPlugins()) + wasmClientKeeper.SetQueryPlugins(keeper.NewDefaultQueryPlugins(GetSimApp(suite.chainA).GRPCQueryRouter())) }) } } diff --git a/modules/light-clients/08-wasm/keeper/snapshotter.go b/modules/light-clients/08-wasm/keeper/snapshotter.go index 2d474386e49..5a4bc97da17 100644 --- a/modules/light-clients/08-wasm/keeper/snapshotter.go +++ b/modules/light-clients/08-wasm/keeper/snapshotter.go @@ -12,7 +12,6 @@ import ( cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" - "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/types" ) @@ -67,13 +66,13 @@ func (ws *WasmSnapshotter) SnapshotExtension(height uint64, payloadWriter snapsh ctx := sdk.NewContext(cacheMS, cmtproto.Header{}, false, nil) - checksums, err := types.GetAllChecksums(ctx) + checksums, err := ws.keeper.GetAllChecksums(ctx) if err != nil { return err } for _, checksum := range checksums { - wasmCode, err := ibcwasm.GetVM().GetCode(checksum) + wasmCode, err := ws.keeper.GetVM().GetCode(checksum) if err != nil { return err } @@ -112,12 +111,12 @@ func restoreV1(ctx sdk.Context, k *Keeper, compressedCode []byte) error { return errorsmod.Wrap(err, "failed to uncompress wasm code") } - checksum, err := ibcwasm.GetVM().StoreCodeUnchecked(wasmCode) + checksum, err := k.GetVM().StoreCodeUnchecked(wasmCode) if err != nil { return errorsmod.Wrap(err, "failed to store wasm code") } - if err := ibcwasm.GetVM().Pin(checksum); err != nil { + if err := k.GetVM().Pin(checksum); err != nil { return errorsmod.Wrapf(err, "failed to pin checksum: %s to in-memory cache", hex.EncodeToString(checksum)) } diff --git a/modules/light-clients/08-wasm/light_client_module.go b/modules/light-clients/08-wasm/light_client_module.go index 0b1376df4cd..7e31029d541 100644 --- a/modules/light-clients/08-wasm/light_client_module.go +++ b/modules/light-clients/08-wasm/light_client_module.go @@ -1,6 +1,11 @@ package wasm import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" @@ -8,6 +13,7 @@ import ( wasmkeeper "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/keeper" "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" ibcerrors "github.com/cosmos/ibc-go/v8/modules/core/errors" "github.com/cosmos/ibc-go/v8/modules/core/exported" ) @@ -34,8 +40,9 @@ func (l *LightClientModule) RegisterStoreProvider(storeProvider exported.ClientS l.storeProvider = storeProvider } -// Initialize unmarshals the provided client and consensus states and performs basic validation. It calls into the -// clientState.Initialize method. +// Initialize unmarshals the provided client and consensus states and performs basic validation. It sets the client +// state and consensus state in the client store. +// It also initializes the wasm contract for the client. // // CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 08-wasm-{n}. func (l LightClientModule) Initialize(ctx sdk.Context, clientID string, clientStateBz, consensusStateBz []byte) error { @@ -58,12 +65,26 @@ func (l LightClientModule) Initialize(ctx sdk.Context, clientID string, clientSt } clientStore := l.storeProvider.ClientStore(ctx, clientID) - cdc := l.keeper.Codec() - return clientState.Initialize(ctx, cdc, clientStore, &consensusState) + // Do not allow initialization of a client with a checksum that hasn't been previously stored via storeWasmCode. + if !l.keeper.HasChecksum(ctx, clientState.Checksum) { + return errorsmod.Wrapf(types.ErrInvalidChecksum, "checksum (%s) has not been previously stored", hex.EncodeToString(clientState.Checksum)) + } + + payload := types.InstantiateMessage{ + ClientState: clientState.Data, + ConsensusState: consensusState.Data, + Checksum: clientState.Checksum, + } + + return l.keeper.WasmInstantiate(ctx, clientID, clientStore, &clientState, payload) } -// VerifyClientMessage obtains the client state associated with the client identifier and calls into the clientState.VerifyClientMessage method. +// VerifyClientMessage obtains the client state associated with the client identifier, it then must verify the ClientMessage. +// A ClientMessage could be a Header, Misbehaviour, or batch update. +// It must handle each type of ClientMessage appropriately. Calls to CheckForMisbehaviour, UpdateState, and UpdateStateOnMisbehaviour +// will assume that the content of the ClientMessage has been verified and can be trusted. An error should be returned +// if the ClientMessage fails to verify. // // CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 08-wasm-{n}. func (l LightClientModule) VerifyClientMessage(ctx sdk.Context, clientID string, clientMsg exported.ClientMessage) error { @@ -75,10 +96,20 @@ func (l LightClientModule) VerifyClientMessage(ctx sdk.Context, clientID string, return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) } - return clientState.VerifyClientMessage(ctx, l.keeper.Codec(), clientStore, clientMsg) + clientMessage, ok := clientMsg.(*types.ClientMessage) + if !ok { + return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected type: %T, got: %T", &types.ClientMessage{}, clientMsg) + } + + payload := types.QueryMsg{ + VerifyClientMessage: &types.VerifyClientMessageMsg{ClientMessage: clientMessage.Data}, + } + _, err := l.keeper.WasmQuery(ctx, clientID, clientStore, clientState, payload) + return err } -// CheckForMisbehaviour obtains the client state associated with the client identifier and calls into the clientState.CheckForMisbehaviour method. +// CheckForMisbehaviour obtains the client state associated with the client identifier, it detects misbehaviour in a submitted Header +// message and verifies the correctness of a submitted Misbehaviour ClientMessage. // // CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 08-wasm-{n}. func (l LightClientModule) CheckForMisbehaviour(ctx sdk.Context, clientID string, clientMsg exported.ClientMessage) bool { @@ -90,10 +121,31 @@ func (l LightClientModule) CheckForMisbehaviour(ctx sdk.Context, clientID string panic(errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID)) } - return clientState.CheckForMisbehaviour(ctx, cdc, clientStore, clientMsg) + clientMessage, ok := clientMsg.(*types.ClientMessage) + if !ok { + return false + } + + payload := types.QueryMsg{ + CheckForMisbehaviour: &types.CheckForMisbehaviourMsg{ClientMessage: clientMessage.Data}, + } + + res, err := l.keeper.WasmQuery(ctx, clientID, clientStore, clientState, payload) + if err != nil { + return false + } + + var result types.CheckForMisbehaviourResult + if err := json.Unmarshal(res, &result); err != nil { + return false + } + + return result.FoundMisbehaviour } -// UpdateStateOnMisbehaviour obtains the client state associated with the client identifier and calls into the clientState.UpdateStateOnMisbehaviour method. +// UpdateStateOnMisbehaviour obtains the client state associated with the client identifier performs appropriate state changes on +// a client state given that misbehaviour has been detected and verified. +// Client state is updated in the store by the contract. // // CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 08-wasm-{n}. func (l LightClientModule) UpdateStateOnMisbehaviour(ctx sdk.Context, clientID string, clientMsg exported.ClientMessage) { @@ -105,10 +157,23 @@ func (l LightClientModule) UpdateStateOnMisbehaviour(ctx sdk.Context, clientID s panic(errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID)) } - clientState.UpdateStateOnMisbehaviour(ctx, cdc, clientStore, clientMsg) + clientMessage, ok := clientMsg.(*types.ClientMessage) + if !ok { + panic(fmt.Errorf("expected type %T, got %T", &types.ClientMessage{}, clientMsg)) + } + + payload := types.SudoMsg{ + UpdateStateOnMisbehaviour: &types.UpdateStateOnMisbehaviourMsg{ClientMessage: clientMessage.Data}, + } + + _, err := l.keeper.WasmSudo(ctx, clientID, clientStore, clientState, payload) + if err != nil { + panic(err) + } } -// UpdateState obtains the client state associated with the client identifier and calls into the clientState.UpdateState method. +// UpdateState obtains the client state associated with the client identifier and calls into the appropriate +// contract endpoint. Client state and new consensus states are updated in the store by the contract. // // CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 08-wasm-{n}. func (l LightClientModule) UpdateState(ctx sdk.Context, clientID string, clientMsg exported.ClientMessage) []exported.Height { @@ -120,10 +185,37 @@ func (l LightClientModule) UpdateState(ctx sdk.Context, clientID string, clientM panic(errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID)) } - return clientState.UpdateState(ctx, cdc, clientStore, clientMsg) + clientMessage, ok := clientMsg.(*types.ClientMessage) + if !ok { + panic(fmt.Errorf("expected type %T, got %T", &types.ClientMessage{}, clientMsg)) + } + + payload := types.SudoMsg{ + UpdateState: &types.UpdateStateMsg{ClientMessage: clientMessage.Data}, + } + + res, err := l.keeper.WasmSudo(ctx, clientID, clientStore, clientState, payload) + if err != nil { + panic(err) + } + + var result types.UpdateStateResult + if err := json.Unmarshal(res, &result); err != nil { + panic(errorsmod.Wrap(types.ErrWasmInvalidResponseData, err.Error())) + } + + heights := []exported.Height{} + for _, height := range result.Heights { + heights = append(heights, height) + } + + return heights } -// VerifyMembership obtains the client state associated with the client identifier and calls into the clientState.VerifyMembership method. +// VerifyMembership obtains the client state associated with the client identifier and calls into the appropriate contract endpoint. +// VerifyMembership is a generic proof verification method which verifies a proof of the existence of a value at a given CommitmentPath at the specified height. +// The caller is expected to construct the full CommitmentPath from a CommitmentPrefix and a standardized path (as defined in ICS 24). +// If a zero proof height is passed in, it will fail to retrieve the associated consensus state. // // CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 08-wasm-{n}. func (l LightClientModule) VerifyMembership( @@ -144,10 +236,41 @@ func (l LightClientModule) VerifyMembership( return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) } - return clientState.VerifyMembership(ctx, clientStore, cdc, height, delayTimePeriod, delayBlockPeriod, proof, path, value) + proofHeight, ok := height.(clienttypes.Height) + if !ok { + return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", clienttypes.Height{}, height) + } + + if clientState.LatestHeight.LT(height) { + return errorsmod.Wrapf( + ibcerrors.ErrInvalidHeight, + "client state height < proof height (%d < %d), please ensure the client has been updated", clientState.LatestHeight, height, + ) + } + + merklePath, ok := path.(commitmenttypes.MerklePath) + if !ok { + return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", commitmenttypes.MerklePath{}, path) + } + + payload := types.SudoMsg{ + VerifyMembership: &types.VerifyMembershipMsg{ + Height: proofHeight, + DelayTimePeriod: delayTimePeriod, + DelayBlockPeriod: delayBlockPeriod, + Proof: proof, + Path: merklePath, + Value: value, + }, + } + _, err := l.keeper.WasmSudo(ctx, clientID, clientStore, clientState, payload) + return err } -// VerifyNonMembership obtains the client state associated with the client identifier and calls into the clientState.VerifyNonMembership method. +// VerifyNonMembership obtains the client state associated with the client identifier and calls into the appropriate contract endpoint. +// VerifyNonMembership is a generic proof verification method which verifies the absence of a given CommitmentPath at a specified height. +// The caller is expected to construct the full CommitmentPath from a CommitmentPrefix and a standardized path (as defined in ICS 24). +// If a zero proof height is passed in, it will fail to retrieve the associated consensus state. // // CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 08-wasm-{n}. func (l LightClientModule) VerifyNonMembership( @@ -167,10 +290,46 @@ func (l LightClientModule) VerifyNonMembership( return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) } - return clientState.VerifyNonMembership(ctx, clientStore, cdc, height, delayTimePeriod, delayBlockPeriod, proof, path) + proofHeight, ok := height.(clienttypes.Height) + if !ok { + return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", clienttypes.Height{}, height) + } + + if clientState.LatestHeight.LT(height) { + return errorsmod.Wrapf( + ibcerrors.ErrInvalidHeight, + "client state height < proof height (%d < %d), please ensure the client has been updated", clientState.LatestHeight, height, + ) + } + + merklePath, ok := path.(commitmenttypes.MerklePath) + if !ok { + return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", commitmenttypes.MerklePath{}, path) + } + + payload := types.SudoMsg{ + VerifyNonMembership: &types.VerifyNonMembershipMsg{ + Height: proofHeight, + DelayTimePeriod: delayTimePeriod, + DelayBlockPeriod: delayBlockPeriod, + Proof: proof, + Path: merklePath, + }, + } + _, err := l.keeper.WasmSudo(ctx, clientID, clientStore, clientState, payload) + return err } -// Status obtains the client state associated with the client identifier and calls into the clientState.Status method. +// Status obtains the client state associated with the client identifier and calls into the appropriate contract endpoint. +// It returns the status of the wasm client. +// The client may be: +// - Active: frozen height is zero and client is not expired +// - Frozen: frozen height is not zero +// - Expired: the latest consensus state timestamp + trusting period <= current time +// - Unauthorized: the client type is not registered as an allowed client type +// +// A frozen client will become expired, so the Frozen status +// has higher precedence. // // CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 08-wasm-{n}. func (l LightClientModule) Status(ctx sdk.Context, clientID string) exported.Status { @@ -182,7 +341,23 @@ func (l LightClientModule) Status(ctx sdk.Context, clientID string) exported.Sta return exported.Unknown } - return clientState.Status(ctx, clientStore, cdc) + // Return unauthorized if the checksum hasn't been previously stored via storeWasmCode. + if !l.keeper.HasChecksum(ctx, clientState.Checksum) { + return exported.Unauthorized + } + + payload := types.QueryMsg{Status: &types.StatusMsg{}} + res, err := l.keeper.WasmQuery(ctx, clientID, clientStore, clientState, payload) + if err != nil { + return exported.Unknown + } + + var result types.StatusResult + if err := json.Unmarshal(res, &result); err != nil { + return exported.Unknown + } + + return exported.Status(result.Status) } // LatestHeight returns the latest height for the client state for the given client identifier. @@ -201,7 +376,8 @@ func (l LightClientModule) LatestHeight(ctx sdk.Context, clientID string) export return clientState.LatestHeight } -// TimestampAtHeight obtains the client state associated with the client identifier and calls into the clientState.GetTimestampAtHeight method. +// TimestampAtHeight obtains the client state associated with the client identifier and calls into the appropriate contract endpoint. +// It returns the timestamp in nanoseconds of the consensus state at the given height. // // CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 08-wasm-{n}. func (l LightClientModule) TimestampAtHeight(ctx sdk.Context, clientID string, height exported.Height) (uint64, error) { @@ -213,11 +389,34 @@ func (l LightClientModule) TimestampAtHeight(ctx sdk.Context, clientID string, h return 0, errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) } - return clientState.GetTimestampAtHeight(ctx, clientStore, cdc, height) + timestampHeight, ok := height.(clienttypes.Height) + if !ok { + return 0, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", clienttypes.Height{}, height) + } + + payload := types.QueryMsg{ + TimestampAtHeight: &types.TimestampAtHeightMsg{ + Height: timestampHeight, + }, + } + + res, err := l.keeper.WasmQuery(ctx, clientID, clientStore, clientState, payload) + if err != nil { + return 0, errorsmod.Wrapf(err, "height (%s)", height) + } + + var result types.TimestampAtHeightResult + if err := json.Unmarshal(res, &result); err != nil { + return 0, errorsmod.Wrapf(types.ErrWasmInvalidResponseData, "failed to unmarshal result of wasm query: %v", err) + } + + return result.Timestamp, nil } // RecoverClient asserts that the substitute client is a wasm client. It obtains the client state associated with the -// subject client and calls into the subjectClientState.CheckSubstituteAndUpdateState method. +// subject client and calls into the appropriate contract endpoint. +// It will verify that a substitute client state is valid and update the subject client state. +// Note that this method is used only for recovery and will not allow changes to the checksum. // // CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 08-wasm-{n}. func (l LightClientModule) RecoverClient(ctx sdk.Context, clientID, substituteClientID string) error { @@ -244,12 +443,27 @@ func (l LightClientModule) RecoverClient(ctx sdk.Context, clientID, substituteCl return errorsmod.Wrap(clienttypes.ErrClientNotFound, substituteClientID) } - return clientState.CheckSubstituteAndUpdateState(ctx, cdc, clientStore, substituteClientStore, substituteClient) + substituteClientState := substituteClient + + // check that checksums of subject client state and substitute client state match + // changing the checksum is only allowed through the migrate contract RPC endpoint + if !bytes.Equal(clientState.Checksum, substituteClientState.Checksum) { + return errorsmod.Wrapf(clienttypes.ErrInvalidClient, "expected checksums to be equal: expected %s, got %s", hex.EncodeToString(clientState.Checksum), hex.EncodeToString(substituteClientState.Checksum)) + } + + store := types.NewMigrateClientWrappedStore(clientStore, substituteClientStore) + + payload := types.SudoMsg{ + MigrateClientStore: &types.MigrateClientStoreMsg{}, + } + + _, err = l.keeper.WasmSudo(ctx, clientID, store, clientState, payload) + return err } -// VerifyUpgradeAndUpdateState obtains the client state associated with the client identifier and calls into the clientState.VerifyUpgradeAndUpdateState method. +// VerifyUpgradeAndUpdateState obtains the client state associated with the client identifier and calls into the appropriate contract endpoint. // The new client and consensus states will be unmarshaled and an error is returned if the new client state is not at a height greater -// than the existing client. +// than the existing client. On a successful verification, it expects the contract to update the new client state, consensus state, and any other client metadata. // // CONTRACT: clientID is validated in 02-client router, thus clientID is assumed here to have the format 08-wasm-{n}. func (l LightClientModule) VerifyUpgradeAndUpdateState( @@ -284,5 +498,15 @@ func (l LightClientModule) VerifyUpgradeAndUpdateState( return errorsmod.Wrapf(ibcerrors.ErrInvalidHeight, "upgraded client height %s must be at greater than current client height %s", newClientState.LatestHeight, lastHeight) } - return clientState.VerifyUpgradeAndUpdateState(ctx, cdc, clientStore, &newClientState, &newConsensusState, upgradeClientProof, upgradeConsensusStateProof) + payload := types.SudoMsg{ + VerifyUpgradeAndUpdateState: &types.VerifyUpgradeAndUpdateStateMsg{ + UpgradeClientState: newClientState.Data, + UpgradeConsensusState: newConsensusState.Data, + ProofUpgradeClient: upgradeClientProof, + ProofUpgradeConsensusState: upgradeConsensusStateProof, + }, + } + + _, err := l.keeper.WasmSudo(ctx, clientID, clientStore, clientState, payload) + return err } diff --git a/modules/light-clients/08-wasm/light_client_module_test.go b/modules/light-clients/08-wasm/light_client_module_test.go index 7a7ecddae55..12b75d007a1 100644 --- a/modules/light-clients/08-wasm/light_client_module_test.go +++ b/modules/light-clients/08-wasm/light_client_module_test.go @@ -11,7 +11,6 @@ import ( errorsmod "cosmossdk.io/errors" - "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" @@ -79,7 +78,8 @@ func (suite *WasmTestSuite) TestStatus() { { "client status is unauthorized: checksum is not stored", func() { - err := ibcwasm.Checksums.Remove(suite.chainA.GetContext(), suite.checksum) + wasmClientKeeper := GetSimApp(suite.chainA).WasmClientKeeper + err := wasmClientKeeper.GetChecksums().Remove(suite.chainA.GetContext(), suite.checksum) suite.Require().NoError(err) }, exported.Unauthorized, @@ -91,6 +91,15 @@ func (suite *WasmTestSuite) TestStatus() { }, exported.Unknown, }, + { + "failure: response fails to unmarshal", + func() { + suite.mockVM.RegisterQueryCallback(types.StatusMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { + return &wasmvmtypes.QueryResult{Ok: []byte("invalid json")}, wasmtesting.DefaultGasUsed, nil + }) + }, + exported.Unknown, + }, } for _, tc := range testCases { @@ -180,6 +189,15 @@ func (suite *WasmTestSuite) TestTimestampAtHeight() { }, clienttypes.ErrClientNotFound, }, + { + "failure: response fails to unmarshal", + func() { + suite.mockVM.RegisterQueryCallback(types.TimestampAtHeightMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { + return &wasmvmtypes.QueryResult{Ok: []byte("invalid json")}, wasmtesting.DefaultGasUsed, nil + }) + }, + types.ErrWasmInvalidResponseData, + }, } for _, tc := range testCases { @@ -1009,6 +1027,16 @@ func (suite *WasmTestSuite) TestCheckForMisbehaviour() { false, // not applicable fmt.Errorf("%s: %s", unusedWasmClientID, clienttypes.ErrClientNotFound), }, + { + "failure: response fails to unmarshal", + func() { + suite.mockVM.RegisterQueryCallback(types.CheckForMisbehaviourMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.QueryResult, uint64, error) { + return &wasmvmtypes.QueryResult{Ok: []byte("invalid json")}, wasmtesting.DefaultGasUsed, nil + }) + }, + false, + nil, + }, } for _, tc := range testCases { @@ -1151,6 +1179,16 @@ func (suite *WasmTestSuite) TestUpdateState() { errorsmod.Wrap(types.ErrVMError, wasmtesting.ErrMockVM.Error()), nil, }, + { + "failure: response fails to unmarshal", + func() { + suite.mockVM.RegisterSudoCallback(types.UpdateStateMsg{}, func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { + return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: []byte("invalid json")}}, wasmtesting.DefaultGasUsed, nil + }) + }, + fmt.Errorf("invalid character 'i' looking for beginning of value: %s", types.ErrWasmInvalidResponseData), + nil, + }, { "failure: callbackFn returns error", func() { diff --git a/modules/light-clients/08-wasm/testing/simapp/app.go b/modules/light-clients/08-wasm/testing/simapp/app.go index 9b95dc649bd..b8fc325bfcd 100644 --- a/modules/light-clients/08-wasm/testing/simapp/app.go +++ b/modules/light-clients/08-wasm/testing/simapp/app.go @@ -846,7 +846,7 @@ func NewSimApp( ctx := app.BaseApp.NewUncachedContext(true, cmtproto.Header{}) // Initialize pinned codes in wasmvm as they are not persisted there - if err := wasmkeeper.InitializePinnedCodes(ctx); err != nil { + if err := app.WasmClientKeeper.InitializePinnedCodes(ctx); err != nil { cmtos.Exit(fmt.Sprintf("failed initialize pinned codes %s", err)) } } diff --git a/modules/light-clients/08-wasm/types/client_state.go b/modules/light-clients/08-wasm/types/client_state.go index 4ce6fe3e044..1053a7dd0ac 100644 --- a/modules/light-clients/08-wasm/types/client_state.go +++ b/modules/light-clients/08-wasm/types/client_state.go @@ -1,17 +1,9 @@ package types import ( - "encoding/hex" - errorsmod "cosmossdk.io/errors" - storetypes "cosmossdk.io/store/types" - - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/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" - ibcerrors "github.com/cosmos/ibc-go/v8/modules/core/errors" "github.com/cosmos/ibc-go/v8/modules/core/exported" ) @@ -43,165 +35,3 @@ func (cs ClientState) Validate() error { return nil } - -// Status returns the status of the wasm client. -// The client may be: -// - Active: frozen height is zero and client is not expired -// - Frozen: frozen height is not zero -// - Expired: the latest consensus state timestamp + trusting period <= current time -// - Unauthorized: the client type is not registered as an allowed client type -// -// A frozen client will become expired, so the Frozen status -// has higher precedence. -func (cs ClientState) Status(ctx sdk.Context, clientStore storetypes.KVStore, _ codec.BinaryCodec) exported.Status { - // Return unauthorized if the checksum hasn't been previously stored via storeWasmCode. - if !HasChecksum(ctx, cs.Checksum) { - return exported.Unauthorized - } - - payload := QueryMsg{Status: &StatusMsg{}} - result, err := wasmQuery[StatusResult](ctx, clientStore, &cs, payload) - if err != nil { - return exported.Unknown - } - - return exported.Status(result.Status) -} - -// GetTimestampAtHeight returns the timestamp in nanoseconds of the consensus state at the given height. -func (cs ClientState) GetTimestampAtHeight( - ctx sdk.Context, - clientStore storetypes.KVStore, - cdc codec.BinaryCodec, - height exported.Height, -) (uint64, error) { - timestampHeight, ok := height.(clienttypes.Height) - if !ok { - return 0, errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", clienttypes.Height{}, height) - } - - payload := QueryMsg{ - TimestampAtHeight: &TimestampAtHeightMsg{ - Height: timestampHeight, - }, - } - - result, err := wasmQuery[TimestampAtHeightResult](ctx, clientStore, &cs, payload) - if err != nil { - return 0, errorsmod.Wrapf(err, "height (%s)", height) - } - - return result.Timestamp, nil -} - -// Initialize checks that the initial consensus state is an 08-wasm consensus state and -// sets the client state, consensus state in the provided client store. -// It also initializes the wasm contract for the client. -func (cs ClientState) Initialize(ctx sdk.Context, cdc codec.BinaryCodec, clientStore storetypes.KVStore, state exported.ConsensusState) error { - consensusState, ok := state.(*ConsensusState) - if !ok { - return errorsmod.Wrapf(clienttypes.ErrInvalidConsensus, "invalid initial consensus state. expected type: %T, got: %T", - &ConsensusState{}, state) - } - - // Do not allow initialization of a client with a checksum that hasn't been previously stored via storeWasmCode. - if !HasChecksum(ctx, cs.Checksum) { - return errorsmod.Wrapf(ErrInvalidChecksum, "checksum (%s) has not been previously stored", hex.EncodeToString(cs.Checksum)) - } - - payload := InstantiateMessage{ - ClientState: cs.Data, - ConsensusState: consensusState.Data, - Checksum: cs.Checksum, - } - - return wasmInstantiate(ctx, cdc, clientStore, &cs, payload) -} - -// VerifyMembership is a generic proof verification method which verifies a proof of the existence of a value at a given CommitmentPath at the specified height. -// The caller is expected to construct the full CommitmentPath from a CommitmentPrefix and a standardized path (as defined in ICS 24). -// If a zero proof height is passed in, it will fail to retrieve the associated consensus state. -func (cs ClientState) VerifyMembership( - ctx sdk.Context, - clientStore storetypes.KVStore, - cdc codec.BinaryCodec, - height exported.Height, - delayTimePeriod uint64, - delayBlockPeriod uint64, - proof []byte, - path exported.Path, - value []byte, -) error { - proofHeight, ok := height.(clienttypes.Height) - if !ok { - return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", clienttypes.Height{}, height) - } - - if cs.LatestHeight.LT(height) { - return errorsmod.Wrapf( - ibcerrors.ErrInvalidHeight, - "client state height < proof height (%d < %d), please ensure the client has been updated", cs.LatestHeight, height, - ) - } - - merklePath, ok := path.(commitmenttypes.MerklePath) - if !ok { - return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", commitmenttypes.MerklePath{}, path) - } - - payload := SudoMsg{ - VerifyMembership: &VerifyMembershipMsg{ - Height: proofHeight, - DelayTimePeriod: delayTimePeriod, - DelayBlockPeriod: delayBlockPeriod, - Proof: proof, - Path: merklePath, - Value: value, - }, - } - _, err := wasmSudo[EmptyResult](ctx, cdc, clientStore, &cs, payload) - return err -} - -// VerifyNonMembership is a generic proof verification method which verifies the absence of a given CommitmentPath at a specified height. -// The caller is expected to construct the full CommitmentPath from a CommitmentPrefix and a standardized path (as defined in ICS 24). -// If a zero proof height is passed in, it will fail to retrieve the associated consensus state. -func (cs ClientState) VerifyNonMembership( - ctx sdk.Context, - clientStore storetypes.KVStore, - cdc codec.BinaryCodec, - height exported.Height, - delayTimePeriod uint64, - delayBlockPeriod uint64, - proof []byte, - path exported.Path, -) error { - proofHeight, ok := height.(clienttypes.Height) - if !ok { - return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", clienttypes.Height{}, height) - } - - if cs.LatestHeight.LT(height) { - return errorsmod.Wrapf( - ibcerrors.ErrInvalidHeight, - "client state height < proof height (%d < %d), please ensure the client has been updated", cs.LatestHeight, height, - ) - } - - merklePath, ok := path.(commitmenttypes.MerklePath) - if !ok { - return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", commitmenttypes.MerklePath{}, path) - } - - payload := SudoMsg{ - VerifyNonMembership: &VerifyNonMembershipMsg{ - Height: proofHeight, - DelayTimePeriod: delayTimePeriod, - DelayBlockPeriod: delayBlockPeriod, - Proof: proof, - Path: merklePath, - }, - } - _, err := wasmSudo[EmptyResult](ctx, cdc, clientStore, &cs, payload) - return err -} diff --git a/modules/light-clients/08-wasm/types/events.go b/modules/light-clients/08-wasm/types/events.go index d2d76f706a1..c8290fe8579 100644 --- a/modules/light-clients/08-wasm/types/events.go +++ b/modules/light-clients/08-wasm/types/events.go @@ -1,13 +1,5 @@ package types -import ( - "cosmossdk.io/log" - - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/cosmos/ibc-go/v8/modules/core/exported" -) - // IBC 08-wasm events const ( // EventTypeStoreWasmCode defines the event type for bytecode storage @@ -24,8 +16,3 @@ const ( AttributeValueCategory = ModuleName ) - -// Logger returns a module-specific logger. -func Logger(ctx sdk.Context) log.Logger { - return ctx.Logger().With("module", "x/"+exported.ModuleName+"-"+ModuleName) -} diff --git a/modules/light-clients/08-wasm/types/export_test.go b/modules/light-clients/08-wasm/types/export_test.go index e8c93990a18..cc0ed8aa270 100644 --- a/modules/light-clients/08-wasm/types/export_test.go +++ b/modules/light-clients/08-wasm/types/export_test.go @@ -2,9 +2,6 @@ package types import ( storetypes "cosmossdk.io/store/types" - - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" ) /* @@ -14,25 +11,8 @@ import ( // MaxWasmSize is the maximum size of a wasm code in bytes. const MaxWasmSize = maxWasmSize -var ( - SubjectPrefix = subjectPrefix - SubstitutePrefix = substitutePrefix -) - -// GetClientID is a wrapper around getClientID to allow the function to be directly called in tests. -func GetClientID(clientStore storetypes.KVStore) (string, error) { - return getClientID(clientStore) -} - -// NewMigrateProposalWrappedStore is a wrapper around newMigrateProposalWrappedStore to allow the function to be directly called in tests. -// -//nolint:revive // Returning unexported type for testing purposes. -func NewMigrateProposalWrappedStore(subjectStore, substituteStore storetypes.KVStore) migrateClientWrappedStore { - return newMigrateClientWrappedStore(subjectStore, substituteStore) -} - // GetStore is a wrapper around getStore to allow the function to be directly called in tests. -func (ws migrateClientWrappedStore) GetStore(key []byte) (storetypes.KVStore, bool) { +func (ws MigrateClientWrappedStore) GetStore(key []byte) (storetypes.KVStore, bool) { return ws.getStore(key) } @@ -40,23 +20,3 @@ func (ws migrateClientWrappedStore) GetStore(key []byte) (storetypes.KVStore, bo func SplitPrefix(key []byte) ([]byte, []byte) { return splitPrefix(key) } - -// WasmQuery wraps wasmQuery and is used solely for testing. -func WasmQuery[T ContractResult](ctx sdk.Context, clientStore storetypes.KVStore, cs *ClientState, payload QueryMsg) (T, error) { - return wasmQuery[T](ctx, clientStore, cs, payload) -} - -// WasmSudo wraps wasmCall and is used solely for testing. -func WasmSudo[T ContractResult](ctx sdk.Context, cdc codec.BinaryCodec, clientStore storetypes.KVStore, cs *ClientState, payload SudoMsg) (T, error) { - return wasmSudo[T](ctx, cdc, clientStore, cs, payload) -} - -// WasmInstantiate wraps wasmInstantiate and is used solely for testing. -func WasmInstantiate(ctx sdk.Context, cdc codec.BinaryCodec, clientStore storetypes.KVStore, cs *ClientState, payload InstantiateMessage) error { - return wasmInstantiate(ctx, cdc, clientStore, cs, payload) -} - -// WasmMigrate wraps wasmMigrate and is used solely for testing. -func WasmMigrate(ctx sdk.Context, cdc codec.BinaryCodec, clientStore storetypes.KVStore, cs *ClientState, clientID string, payload []byte) error { - return wasmMigrate(ctx, cdc, clientStore, cs, clientID, payload) -} diff --git a/modules/light-clients/08-wasm/types/gas_register.go b/modules/light-clients/08-wasm/types/gas_register.go index 54f15bc83fc..512df7a8b0f 100644 --- a/modules/light-clients/08-wasm/types/gas_register.go +++ b/modules/light-clients/08-wasm/types/gas_register.go @@ -63,10 +63,13 @@ const ( // default: 0.15 gas. // see https://github.com/CosmWasm/wasmd/pull/898#discussion_r937727200 -var defaultPerByteUncompressCost = wasmvmtypes.UFraction{ - Numerator: 15, - Denominator: 100, -} +var ( + defaultPerByteUncompressCost = wasmvmtypes.UFraction{ + Numerator: 15, + Denominator: 100, + } + VMGasRegister = NewDefaultWasmGasRegister() +) // DefaultPerByteUncompressCost is how much SDK gas we charge per source byte to unpack func DefaultPerByteUncompressCost() wasmvmtypes.UFraction { diff --git a/modules/light-clients/08-wasm/types/gas_register_custom.go b/modules/light-clients/08-wasm/types/gas_register_custom.go index 80b41663f2a..991d8779c43 100644 --- a/modules/light-clients/08-wasm/types/gas_register_custom.go +++ b/modules/light-clients/08-wasm/types/gas_register_custom.go @@ -19,7 +19,7 @@ const ( DefaultDeserializationCostPerByte = 1 ) -var costJSONDeserialization = wasmvmtypes.UFraction{ +var CostJSONDeserialization = wasmvmtypes.UFraction{ Numerator: DefaultDeserializationCostPerByte * DefaultGasMultiplier, Denominator: 1, } diff --git a/modules/light-clients/08-wasm/types/keys.go b/modules/light-clients/08-wasm/types/keys.go index a4a9d90e972..a2a159233c4 100644 --- a/modules/light-clients/08-wasm/types/keys.go +++ b/modules/light-clients/08-wasm/types/keys.go @@ -1,5 +1,7 @@ package types +import "cosmossdk.io/collections" + const ( // ModuleName for the wasm client ModuleName = "08-wasm" @@ -14,3 +16,6 @@ const ( // Deprecated: in favor of collections.KeySet KeyChecksums = "checksums" ) + +// ChecksumsKey is the key under which all checksums are stored +var ChecksumsKey = collections.NewPrefix(0) diff --git a/modules/light-clients/08-wasm/types/migrate_contract.go b/modules/light-clients/08-wasm/types/migrate_contract.go deleted file mode 100644 index f028c2c02fa..00000000000 --- a/modules/light-clients/08-wasm/types/migrate_contract.go +++ /dev/null @@ -1,41 +0,0 @@ -package types - -import ( - "bytes" - "encoding/hex" - - errorsmod "cosmossdk.io/errors" - storetypes "cosmossdk.io/store/types" - - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// MigrateContract calls the migrate entry point on the contract with the given -// migrateMsg. The contract must exist and the checksum must be found in the -// store. If the checksum is the same as the current checksum, an error is returned. -// This does not update the checksum in the client state. -func (cs ClientState) MigrateContract( - ctx sdk.Context, cdc codec.BinaryCodec, clientStore storetypes.KVStore, - clientID string, newChecksum, migrateMsg []byte, -) error { - if !HasChecksum(ctx, newChecksum) { - return ErrWasmChecksumNotFound - } - - if bytes.Equal(cs.Checksum, newChecksum) { - return errorsmod.Wrapf(ErrWasmCodeExists, "new checksum (%s) is the same as current checksum (%s)", hex.EncodeToString(newChecksum), hex.EncodeToString(cs.Checksum)) - } - - // update the checksum, this needs to be done before the contract migration - // so that wasmMigrate can call the right code. Note that this is not - // persisted to the client store. - cs.Checksum = newChecksum - - err := wasmMigrate(ctx, cdc, clientStore, &cs, clientID, migrateMsg) - if err != nil { - return err - } - - return nil -} diff --git a/modules/light-clients/08-wasm/types/migrate_contract_test.go b/modules/light-clients/08-wasm/types/migrate_contract_test.go deleted file mode 100644 index 78724712888..00000000000 --- a/modules/light-clients/08-wasm/types/migrate_contract_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package types_test - -import ( - "encoding/hex" - "encoding/json" - - wasmvm "github.com/CosmWasm/wasmvm/v2" - wasmvmtypes "github.com/CosmWasm/wasmvm/v2/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.ContractResult, 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.ContractResult{Ok: &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.ContractResult, uint64, error) { - expClientState = types.NewClientState([]byte{1}, newHash, clienttypes.NewHeight(2000, 2)) - store.Set(host.ClientStateKey(), clienttypes.MustMarshalClientState(suite.chainA.App.AppCodec(), expClientState)) - - data, err := json.Marshal(types.EmptyResult{}) - suite.Require().NoError(err) - - return &wasmvmtypes.ContractResult{Ok: &wasmvmtypes.Response{Data: data}}, wasmtesting.DefaultGasUsed, nil - } - }, - nil, - }, - { - "failure: new and old checksum are the same", - func() { - newHash = oldHash - // this should not be called - suite.mockVM.MigrateFn = func(_ wasmvm.Checksum, _ wasmvmtypes.Env, _ []byte, _ wasmvm.KVStore, _ wasmvm.GoAPI, _ wasmvm.Querier, _ wasmvm.GasMeter, _ uint64, _ wasmvmtypes.UFraction) (*wasmvmtypes.ContractResult, uint64, error) { - panic("unreachable") - } - }, - types.ErrWasmCodeExists, - }, - { - "failure: checksum not found", - func() { - err := ibcwasm.Checksums.Remove(suite.chainA.GetContext(), newHash) - suite.Require().NoError(err) - }, - types.ErrWasmChecksumNotFound, - }, - { - "failure: vm 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.ContractResult, uint64, error) { - return nil, wasmtesting.DefaultGasUsed, wasmtesting.ErrMockVM - } - }, - types.ErrVMError, - }, - { - "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.ContractResult, uint64, error) { - return &wasmvmtypes.ContractResult{Err: wasmtesting.ErrMockContract.Error()}, wasmtesting.DefaultGasUsed, nil - } - }, - types.ErrWasmContractCallFailed, - }, - } - - for _, tc := range testCases { - tc := tc - suite.Run(tc.name, func() { - suite.SetupWasmWithMockVM() - - var err error - oldHash, err = types.CreateChecksum(wasmtesting.Code) - suite.Require().NoError(err) - newHash, err = types.CreateChecksum(wasmtesting.CreateMockContract([]byte{1, 2, 3})) - suite.Require().NoError(err) - - 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/misbehaviour_handle.go b/modules/light-clients/08-wasm/types/misbehaviour_handle.go deleted file mode 100644 index 3923287d523..00000000000 --- a/modules/light-clients/08-wasm/types/misbehaviour_handle.go +++ /dev/null @@ -1,30 +0,0 @@ -package types - -import ( - storetypes "cosmossdk.io/store/types" - - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" - - "github.com/cosmos/ibc-go/v8/modules/core/exported" -) - -// CheckForMisbehaviour detects misbehaviour in a submitted Header message and verifies -// the correctness of a submitted Misbehaviour ClientMessage -func (cs ClientState) CheckForMisbehaviour(ctx sdk.Context, _ codec.BinaryCodec, clientStore storetypes.KVStore, clientMsg exported.ClientMessage) bool { - clientMessage, ok := clientMsg.(*ClientMessage) - if !ok { - return false - } - - payload := QueryMsg{ - CheckForMisbehaviour: &CheckForMisbehaviourMsg{ClientMessage: clientMessage.Data}, - } - - result, err := wasmQuery[CheckForMisbehaviourResult](ctx, clientStore, &cs, payload) - if err != nil { - return false - } - - return result.FoundMisbehaviour -} diff --git a/modules/light-clients/08-wasm/types/proposal_handle.go b/modules/light-clients/08-wasm/types/proposal_handle.go deleted file mode 100644 index 73fed826c1f..00000000000 --- a/modules/light-clients/08-wasm/types/proposal_handle.go +++ /dev/null @@ -1,42 +0,0 @@ -package types - -import ( - "bytes" - "encoding/hex" - - errorsmod "cosmossdk.io/errors" - storetypes "cosmossdk.io/store/types" - - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" - - clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" - "github.com/cosmos/ibc-go/v8/modules/core/exported" -) - -// CheckSubstituteAndUpdateState will verify that a substitute client state is valid and update the subject client state. -// Note that this method is used only for recovery and will not allow changes to the checksum. -func (cs ClientState) CheckSubstituteAndUpdateState(ctx sdk.Context, cdc codec.BinaryCodec, subjectClientStore, substituteClientStore storetypes.KVStore, substituteClient exported.ClientState) error { - substituteClientState, ok := substituteClient.(*ClientState) - if !ok { - return errorsmod.Wrapf( - clienttypes.ErrInvalidClient, - "invalid substitute client state: expected type %T, got %T", &ClientState{}, substituteClient, - ) - } - - // check that checksums of subject client state and substitute client state match - // changing the checksum is only allowed through the migrate contract RPC endpoint - if !bytes.Equal(cs.Checksum, substituteClientState.Checksum) { - return errorsmod.Wrapf(clienttypes.ErrInvalidClient, "expected checksums to be equal: expected %s, got %s", hex.EncodeToString(cs.Checksum), hex.EncodeToString(substituteClientState.Checksum)) - } - - store := newMigrateClientWrappedStore(subjectClientStore, substituteClientStore) - - payload := SudoMsg{ - MigrateClientStore: &MigrateClientStoreMsg{}, - } - - _, err := wasmSudo[EmptyResult](ctx, cdc, store, &cs, payload) - return err -} diff --git a/modules/light-clients/08-wasm/types/store.go b/modules/light-clients/08-wasm/types/store.go index 0bf9c628705..bb5f5f785e8 100644 --- a/modules/light-clients/08-wasm/types/store.go +++ b/modules/light-clients/08-wasm/types/store.go @@ -2,34 +2,28 @@ package types import ( "bytes" - "context" "errors" "io" - "reflect" - "strings" wasmvm "github.com/CosmWasm/wasmvm/v2" wasmvmtypes "github.com/CosmWasm/wasmvm/v2/types" - errorsmod "cosmossdk.io/errors" "cosmossdk.io/store/cachekv" - storeprefix "cosmossdk.io/store/prefix" "cosmossdk.io/store/tracekv" storetypes "cosmossdk.io/store/types" "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/internal/ibcwasm" clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" host "github.com/cosmos/ibc-go/v8/modules/core/24-host" ) var ( - _ wasmvmtypes.KVStore = &storeAdapter{} - _ storetypes.KVStore = &migrateClientWrappedStore{} + _ wasmvmtypes.KVStore = &StoreAdapter{} + _ storetypes.KVStore = &MigrateClientWrappedStore{} - subjectPrefix = []byte("subject/") - substitutePrefix = []byte("substitute/") + SubjectPrefix = []byte("subject/") + SubstitutePrefix = []byte("substitute/") ) // GetClientState retrieves the client state from the store using the provided KVStore and codec. @@ -56,49 +50,17 @@ 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 -} - -// migrateClientWrappedStore combines two KVStores into one. +// MigrateClientWrappedStore combines two KVStores into one. // // Both stores are used for reads, but only the subjectStore is used for writes. For all operations, the key // is checked to determine which store to use and must be prefixed with either "subject/" or "substitute/" accordingly. // If the key is not prefixed with either "subject/" or "substitute/", a default action is taken (e.g. no-op for Set/Delete). -type migrateClientWrappedStore struct { +type MigrateClientWrappedStore struct { subjectStore storetypes.KVStore substituteStore storetypes.KVStore } -func newMigrateClientWrappedStore(subjectStore, substituteStore storetypes.KVStore) migrateClientWrappedStore { +func NewMigrateClientWrappedStore(subjectStore, substituteStore storetypes.KVStore) MigrateClientWrappedStore { if subjectStore == nil { panic(errors.New("subjectStore must not be nil")) } @@ -106,7 +68,7 @@ func newMigrateClientWrappedStore(subjectStore, substituteStore storetypes.KVSto panic(errors.New("substituteStore must not be nil")) } - return migrateClientWrappedStore{ + return MigrateClientWrappedStore{ subjectStore: subjectStore, substituteStore: substituteStore, } @@ -115,7 +77,7 @@ func newMigrateClientWrappedStore(subjectStore, substituteStore storetypes.KVSto // Get implements the storetypes.KVStore interface. It allows reads from both the subjectStore and substituteStore. // // Get will return an empty byte slice if the key is not prefixed with either "subject/" or "substitute/". -func (ws migrateClientWrappedStore) Get(key []byte) []byte { +func (ws MigrateClientWrappedStore) Get(key []byte) []byte { prefix, key := splitPrefix(key) store, found := ws.getStore(prefix) @@ -130,7 +92,7 @@ func (ws migrateClientWrappedStore) Get(key []byte) []byte { // Has implements the storetypes.KVStore interface. It allows reads from both the subjectStore and substituteStore. // // Note: contracts do not have access to the Has method, it is only implemented here to satisfy the storetypes.KVStore interface. -func (ws migrateClientWrappedStore) Has(key []byte) bool { +func (ws MigrateClientWrappedStore) Has(key []byte) bool { prefix, key := splitPrefix(key) store, found := ws.getStore(prefix) @@ -145,9 +107,9 @@ func (ws migrateClientWrappedStore) Has(key []byte) bool { // Set implements the storetypes.KVStore interface. It allows writes solely to the subjectStore. // // Set will no-op if the key is not prefixed with "subject/". -func (ws migrateClientWrappedStore) Set(key, value []byte) { +func (ws MigrateClientWrappedStore) Set(key, value []byte) { prefix, key := splitPrefix(key) - if !bytes.Equal(prefix, subjectPrefix) { + if !bytes.Equal(prefix, SubjectPrefix) { return // no-op } @@ -157,9 +119,9 @@ func (ws migrateClientWrappedStore) Set(key, value []byte) { // Delete implements the storetypes.KVStore interface. It allows deletions solely to the subjectStore. // // Delete will no-op if the key is not prefixed with "subject/". -func (ws migrateClientWrappedStore) Delete(key []byte) { +func (ws MigrateClientWrappedStore) Delete(key []byte) { prefix, key := splitPrefix(key) - if !bytes.Equal(prefix, subjectPrefix) { + if !bytes.Equal(prefix, SubjectPrefix) { return // no-op } @@ -169,7 +131,7 @@ func (ws migrateClientWrappedStore) Delete(key []byte) { // Iterator implements the storetypes.KVStore interface. It allows iteration over both the subjectStore and substituteStore. // // Iterator will return a closed iterator if the start or end keys are not prefixed with either "subject/" or "substitute/". -func (ws migrateClientWrappedStore) Iterator(start, end []byte) storetypes.Iterator { +func (ws MigrateClientWrappedStore) Iterator(start, end []byte) storetypes.Iterator { prefixStart, start := splitPrefix(start) prefixEnd, end := splitPrefix(end) @@ -188,7 +150,7 @@ func (ws migrateClientWrappedStore) Iterator(start, end []byte) storetypes.Itera // ReverseIterator implements the storetypes.KVStore interface. It allows iteration over both the subjectStore and substituteStore. // // ReverseIterator will return a closed iterator if the start or end keys are not prefixed with either "subject/" or "substitute/". -func (ws migrateClientWrappedStore) ReverseIterator(start, end []byte) storetypes.Iterator { +func (ws MigrateClientWrappedStore) ReverseIterator(start, end []byte) storetypes.Iterator { prefixStart, start := splitPrefix(start) prefixEnd, end := splitPrefix(end) @@ -205,17 +167,17 @@ func (ws migrateClientWrappedStore) ReverseIterator(start, end []byte) storetype } // GetStoreType implements the storetypes.KVStore interface, it is implemented solely to satisfy the interface. -func (ws migrateClientWrappedStore) GetStoreType() storetypes.StoreType { +func (ws MigrateClientWrappedStore) GetStoreType() storetypes.StoreType { return ws.substituteStore.GetStoreType() } // CacheWrap implements the storetypes.KVStore interface, it is implemented solely to satisfy the interface. -func (ws migrateClientWrappedStore) CacheWrap() storetypes.CacheWrap { +func (ws MigrateClientWrappedStore) CacheWrap() storetypes.CacheWrap { return cachekv.NewStore(ws) } // CacheWrapWithTrace implements the storetypes.KVStore interface, it is implemented solely to satisfy the interface. -func (ws migrateClientWrappedStore) CacheWrapWithTrace(w io.Writer, tc storetypes.TraceContext) storetypes.CacheWrap { +func (ws MigrateClientWrappedStore) CacheWrapWithTrace(w io.Writer, tc storetypes.TraceContext) storetypes.CacheWrap { return cachekv.NewStore(tracekv.NewStore(ws, w, tc)) } @@ -224,10 +186,10 @@ func (ws migrateClientWrappedStore) CacheWrapWithTrace(w io.Writer, tc storetype // the substituteStore is returned. // // If the key is not prefixed with either "subject/" or "substitute/", a nil store is returned and the boolean flag is false. -func (ws migrateClientWrappedStore) getStore(prefix []byte) (storetypes.KVStore, bool) { - if bytes.Equal(prefix, subjectPrefix) { +func (ws MigrateClientWrappedStore) getStore(prefix []byte) (storetypes.KVStore, bool) { + if bytes.Equal(prefix, SubjectPrefix) { return ws.subjectStore, true - } else if bytes.Equal(prefix, substitutePrefix) { + } else if bytes.Equal(prefix, SubstitutePrefix) { return ws.substituteStore, true } @@ -236,7 +198,7 @@ func (ws migrateClientWrappedStore) getStore(prefix []byte) (storetypes.KVStore, // closedIterator returns an iterator that is always closed, used when Iterator() or ReverseIterator() is called // with an invalid prefix or start/end key. -func (ws migrateClientWrappedStore) closedIterator() storetypes.Iterator { +func (ws MigrateClientWrappedStore) closedIterator() storetypes.Iterator { // Create a dummy iterator that is always closed right away. it := ws.subjectStore.Iterator([]byte{0}, []byte{1}) it.Close() @@ -247,93 +209,49 @@ func (ws migrateClientWrappedStore) closedIterator() storetypes.Iterator { // splitPrefix splits the key into the prefix and the key itself, if the key is prefixed with either "subject/" or "substitute/". // If the key is not prefixed with either "subject/" or "substitute/", the prefix is nil. func splitPrefix(key []byte) ([]byte, []byte) { - if bytes.HasPrefix(key, subjectPrefix) { - return subjectPrefix, bytes.TrimPrefix(key, subjectPrefix) - } else if bytes.HasPrefix(key, substitutePrefix) { - return substitutePrefix, bytes.TrimPrefix(key, substitutePrefix) + if bytes.HasPrefix(key, SubjectPrefix) { + return SubjectPrefix, bytes.TrimPrefix(key, SubjectPrefix) + } else if bytes.HasPrefix(key, SubstitutePrefix) { + return SubstitutePrefix, bytes.TrimPrefix(key, SubstitutePrefix) } return nil, key } -// storeAdapter bridges the SDK store implementation to wasmvm one. It implements the wasmvmtypes.KVStore interface. -type storeAdapter struct { +// StoreAdapter bridges the SDK store implementation to wasmvm one. It implements the wasmvmtypes.KVStore interface. +type StoreAdapter struct { parent storetypes.KVStore } -// newStoreAdapter constructor -func newStoreAdapter(s storetypes.KVStore) *storeAdapter { +// NewStoreAdapter constructor +func NewStoreAdapter(s storetypes.KVStore) *StoreAdapter { if s == nil { panic(errors.New("store must not be nil")) } - return &storeAdapter{parent: s} + return &StoreAdapter{parent: s} } // Get implements the wasmvmtypes.KVStore interface. -func (s storeAdapter) Get(key []byte) []byte { +func (s StoreAdapter) Get(key []byte) []byte { return s.parent.Get(key) } // Set implements the wasmvmtypes.KVStore interface. -func (s storeAdapter) Set(key, value []byte) { +func (s StoreAdapter) Set(key, value []byte) { s.parent.Set(key, value) } // Delete implements the wasmvmtypes.KVStore interface. -func (s storeAdapter) Delete(key []byte) { +func (s StoreAdapter) Delete(key []byte) { s.parent.Delete(key) } // Iterator implements the wasmvmtypes.KVStore interface. -func (s storeAdapter) Iterator(start, end []byte) wasmvmtypes.Iterator { +func (s StoreAdapter) Iterator(start, end []byte) wasmvmtypes.Iterator { return s.parent.Iterator(start, end) } // ReverseIterator implements the wasmvmtypes.KVStore interface. -func (s storeAdapter) ReverseIterator(start, end []byte) wasmvmtypes.Iterator { +func (s StoreAdapter) ReverseIterator(start, end []byte) wasmvmtypes.Iterator { return s.parent.ReverseIterator(start, end) } - -// getClientID extracts and validates the clientID from the clientStore's prefix. -// -// Due to the 02-client module not passing the clientID to the 08-wasm module, -// this function was devised to infer it from the store's prefix. -// The expected format of the clientStore prefix is "/{clientID}/". -// If the clientStore is of type migrateProposalWrappedStore, the subjectStore's prefix is utilized instead. -func getClientID(clientStore storetypes.KVStore) (string, error) { - upws, isMigrateProposalWrappedStore := clientStore.(migrateClientWrappedStore) - if isMigrateProposalWrappedStore { - // if the clientStore is a migrateProposalWrappedStore, we retrieve the subjectStore - // because the contract call will be made on the client with the ID of the subjectStore - clientStore = upws.subjectStore - } - - store, ok := clientStore.(storeprefix.Store) - if !ok { - return "", errorsmod.Wrap(ErrRetrieveClientID, "clientStore is not a prefix store") - } - - // using reflect to retrieve the private prefix field - r := reflect.ValueOf(&store).Elem() - - f := r.FieldByName("prefix") - if !f.IsValid() { - return "", errorsmod.Wrap(ErrRetrieveClientID, "prefix field not found") - } - - prefix := string(f.Bytes()) - - split := strings.Split(prefix, "/") - if len(split) < 3 { - return "", errorsmod.Wrap(ErrRetrieveClientID, "prefix is not of the expected form") - } - - // the clientID is the second to last element of the prefix - // the prefix is expected to be of the form "/{clientID}/" - clientID := split[len(split)-2] - if err := ValidateClientID(clientID); err != nil { - return "", errorsmod.Wrapf(ErrRetrieveClientID, "prefix does not contain a valid clientID: %s", err.Error()) - } - - return clientID, nil -} diff --git a/modules/light-clients/08-wasm/types/store_test.go b/modules/light-clients/08-wasm/types/store_test.go index 7037ccbe0aa..0633caf5747 100644 --- a/modules/light-clients/08-wasm/types/store_test.go +++ b/modules/light-clients/08-wasm/types/store_test.go @@ -1,10 +1,8 @@ package types_test import ( - prefixstore "cosmossdk.io/store/prefix" 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" host "github.com/cosmos/ibc-go/v8/modules/core/24-host" @@ -12,123 +10,6 @@ import ( var invalidPrefix = []byte("invalid/") -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) - }) - } -} - // TestMigrateClientWrappedStoreGetStore tests the getStore method of the migrateClientWrappedStore. func (suite *TypesTestSuite) TestMigrateClientWrappedStoreGetStore() { // calls suite.SetupWasmWithMockVM() and creates two clients with their respective stores @@ -164,7 +45,7 @@ func (suite *TypesTestSuite) TestMigrateClientWrappedStoreGetStore() { for _, tc := range testCases { tc := tc suite.Run(tc.name, func() { - wrappedStore := types.NewMigrateProposalWrappedStore(subjectStore, substituteStore) + wrappedStore := types.NewMigrateClientWrappedStore(subjectStore, substituteStore) store, found := wrappedStore.GetStore(tc.prefix) @@ -256,7 +137,7 @@ func (suite *TypesTestSuite) TestMigrateClientWrappedStoreGet() { for _, tc := range testCases { tc := tc suite.Run(tc.name, func() { - wrappedStore := types.NewMigrateProposalWrappedStore(subjectStore, substituteStore) + wrappedStore := types.NewMigrateClientWrappedStore(subjectStore, substituteStore) prefixedKey := tc.prefix prefixedKey = append(prefixedKey, tc.key...) @@ -301,7 +182,7 @@ func (suite *TypesTestSuite) TestMigrateClientWrappedStoreSet() { suite.Run(tc.name, func() { // calls suite.SetupWasmWithMockVM() and creates two clients with their respective stores subjectStore, substituteStore := suite.GetSubjectAndSubstituteStore() - wrappedStore := types.NewMigrateProposalWrappedStore(subjectStore, substituteStore) + wrappedStore := types.NewMigrateClientWrappedStore(subjectStore, substituteStore) prefixedKey := tc.prefix prefixedKey = append(prefixedKey, tc.key...) @@ -352,7 +233,7 @@ func (suite *TypesTestSuite) TestMigrateClientWrappedStoreDelete() { suite.Run(tc.name, func() { // calls suite.SetupWasmWithMockVM() and creates two clients with their respective stores subjectStore, substituteStore := suite.GetSubjectAndSubstituteStore() - wrappedStore := types.NewMigrateProposalWrappedStore(subjectStore, substituteStore) + wrappedStore := types.NewMigrateClientWrappedStore(subjectStore, substituteStore) prefixedKey := tc.prefix prefixedKey = append(prefixedKey, tc.key...) @@ -424,7 +305,7 @@ func (suite *TypesTestSuite) TestMigrateClientWrappedStoreIterators() { for _, tc := range testCases { tc := tc suite.Run(tc.name, func() { - wrappedStore := types.NewMigrateProposalWrappedStore(subjectStore, substituteStore) + wrappedStore := types.NewMigrateClientWrappedStore(subjectStore, substituteStore) prefixedKeyStart := tc.prefixStart prefixedKeyStart = append(prefixedKeyStart, tc.start...) @@ -481,90 +362,17 @@ func (suite *TypesTestSuite) TestNewMigrateClientWrappedStore() { expPass := !tc.expPanic if expPass { suite.Require().NotPanics(func() { - types.NewMigrateProposalWrappedStore(subjectStore, substituteStore) + types.NewMigrateClientWrappedStore(subjectStore, substituteStore) }) } else { suite.Require().Panics(func() { - types.NewMigrateProposalWrappedStore(subjectStore, substituteStore) + types.NewMigrateClientWrappedStore(subjectStore, substituteStore) }) } }) } } -func (suite *TypesTestSuite) TestGetClientID() { - clientStore := suite.chainA.App.GetIBCKeeper().ClientKeeper.ClientStore(suite.chainA.GetContext(), defaultWasmClientID) - - testCases := []struct { - name string - malleate func() - expError error - }{ - { - "success: clientID retrieved", - func() {}, - nil, - }, - { - "success: clientID retrieved from migrateClientWrappedStore", - func() { - // substituteStore is ignored. - clientStore = types.NewMigrateProposalWrappedStore(clientStore, clientStore) - }, - nil, - }, - { - "failure: clientStore is nil", - func() { - clientStore = nil - }, - types.ErrRetrieveClientID, - }, - { - "failure: prefix store does not contain prefix", - func() { - clientStore = prefixstore.NewStore(nil, nil) - }, - types.ErrRetrieveClientID, - }, - { - "failure: prefix does not contain slash separated path", - func() { - clientStore = prefixstore.NewStore(nil, []byte("not-a-slash-separated-path")) - }, - types.ErrRetrieveClientID, - }, - { - "failure: prefix only contains one slash", - func() { - clientStore = prefixstore.NewStore(nil, []byte("only-one-slash/")) - }, - types.ErrRetrieveClientID, - }, - { - "failure: prefix does not contain a wasm clientID", - func() { - clientStore = prefixstore.NewStore(nil, []byte("/not-client-id/")) - }, - types.ErrRetrieveClientID, - }, - } - - for _, tc := range testCases { - suite.Run(tc.name, func() { - tc.malleate() - clientID, err := types.GetClientID(clientStore) - - if tc.expError == nil { - suite.Require().NoError(err) - suite.Require().Equal(defaultWasmClientID, clientID) - } else { - suite.Require().ErrorIs(err, tc.expError) - } - }) - } -} - // GetSubjectAndSubstituteStore returns a subject and substitute store for testing. func (suite *TypesTestSuite) GetSubjectAndSubstituteStore() (storetypes.KVStore, storetypes.KVStore) { suite.SetupWasmWithMockVM() diff --git a/modules/light-clients/08-wasm/types/update.go b/modules/light-clients/08-wasm/types/update.go deleted file mode 100644 index 20d2bfb8e4a..00000000000 --- a/modules/light-clients/08-wasm/types/update.go +++ /dev/null @@ -1,75 +0,0 @@ -package types - -import ( - "fmt" - - errorsmod "cosmossdk.io/errors" - storetypes "cosmossdk.io/store/types" - - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" - - ibcerrors "github.com/cosmos/ibc-go/v8/modules/core/errors" - "github.com/cosmos/ibc-go/v8/modules/core/exported" -) - -var _ exported.ClientState = (*ClientState)(nil) - -// VerifyClientMessage must verify a ClientMessage. A ClientMessage could be a Header, Misbehaviour, or batch update. -// It must handle each type of ClientMessage appropriately. Calls to CheckForMisbehaviour, UpdateState, and UpdateStateOnMisbehaviour -// will assume that the content of the ClientMessage has been verified and can be trusted. An error should be returned -// if the ClientMessage fails to verify. -func (cs ClientState) VerifyClientMessage(ctx sdk.Context, _ codec.BinaryCodec, clientStore storetypes.KVStore, clientMsg exported.ClientMessage) error { - clientMessage, ok := clientMsg.(*ClientMessage) - if !ok { - return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected type: %T, got: %T", &ClientMessage{}, clientMsg) - } - - payload := QueryMsg{ - VerifyClientMessage: &VerifyClientMessageMsg{ClientMessage: clientMessage.Data}, - } - _, err := wasmQuery[EmptyResult](ctx, clientStore, &cs, payload) - return err -} - -// Client state and new consensus states are updated in the store by the contract -func (cs ClientState) UpdateState(ctx sdk.Context, cdc codec.BinaryCodec, clientStore storetypes.KVStore, clientMsg exported.ClientMessage) []exported.Height { - clientMessage, ok := clientMsg.(*ClientMessage) - if !ok { - panic(fmt.Errorf("expected type %T, got %T", &ClientMessage{}, clientMsg)) - } - - payload := SudoMsg{ - UpdateState: &UpdateStateMsg{ClientMessage: clientMessage.Data}, - } - - result, err := wasmSudo[UpdateStateResult](ctx, cdc, clientStore, &cs, payload) - if err != nil { - panic(err) - } - - heights := []exported.Height{} - for _, height := range result.Heights { - heights = append(heights, height) - } - - return heights -} - -// UpdateStateOnMisbehaviour should perform appropriate state changes on a client state given that misbehaviour has been detected and verified -// Client state is updated in the store by contract. -func (cs ClientState) UpdateStateOnMisbehaviour(ctx sdk.Context, cdc codec.BinaryCodec, clientStore storetypes.KVStore, clientMsg exported.ClientMessage) { - clientMessage, ok := clientMsg.(*ClientMessage) - if !ok { - panic(fmt.Errorf("expected type %T, got %T", &ClientMessage{}, clientMsg)) - } - - payload := SudoMsg{ - UpdateStateOnMisbehaviour: &UpdateStateOnMisbehaviourMsg{ClientMessage: clientMessage.Data}, - } - - _, err := wasmSudo[EmptyResult](ctx, cdc, clientStore, &cs, payload) - if err != nil { - panic(err) - } -} diff --git a/modules/light-clients/08-wasm/types/upgrade.go b/modules/light-clients/08-wasm/types/upgrade.go deleted file mode 100644 index 6f4ea5ef589..00000000000 --- a/modules/light-clients/08-wasm/types/upgrade.go +++ /dev/null @@ -1,48 +0,0 @@ -package types - -import ( - errorsmod "cosmossdk.io/errors" - storetypes "cosmossdk.io/store/types" - - "github.com/cosmos/cosmos-sdk/codec" - sdk "github.com/cosmos/cosmos-sdk/types" - - clienttypes "github.com/cosmos/ibc-go/v8/modules/core/02-client/types" - "github.com/cosmos/ibc-go/v8/modules/core/exported" -) - -// VerifyUpgradeAndUpdateState, on a successful verification expects the contract to update -// the new client state, consensus state, and any other client metadata. -func (cs ClientState) VerifyUpgradeAndUpdateState( - ctx sdk.Context, - cdc codec.BinaryCodec, - clientStore storetypes.KVStore, - upgradedClient exported.ClientState, - upgradedConsState exported.ConsensusState, - upgradeClientProof, - upgradeConsensusStateProof []byte, -) error { - wasmUpgradeClientState, ok := upgradedClient.(*ClientState) - if !ok { - return errorsmod.Wrapf(clienttypes.ErrInvalidClient, "upgraded client state must be wasm light client state. expected %T, got: %T", - &ClientState{}, wasmUpgradeClientState) - } - - wasmUpgradeConsState, ok := upgradedConsState.(*ConsensusState) - if !ok { - return errorsmod.Wrapf(clienttypes.ErrInvalidConsensus, "upgraded consensus state must be wasm light consensus state. expected %T, got: %T", - &ConsensusState{}, wasmUpgradeConsState) - } - - payload := SudoMsg{ - VerifyUpgradeAndUpdateState: &VerifyUpgradeAndUpdateStateMsg{ - UpgradeClientState: wasmUpgradeClientState.Data, - UpgradeConsensusState: wasmUpgradeConsState.Data, - ProofUpgradeClient: upgradeClientProof, - ProofUpgradeConsensusState: upgradeConsensusStateProof, - }, - } - - _, err := wasmSudo[EmptyResult](ctx, cdc, clientStore, &cs, payload) - return err -} From 9768c10af37e850f2d5cf46ac071825375676c81 Mon Sep 17 00:00:00 2001 From: DimitrisJim Date: Thu, 11 Apr 2024 00:22:23 +0300 Subject: [PATCH 3/6] chore: address remaining TODOs. --- .../light-clients/08-wasm/keeper/querier.go | 1 - .../08-wasm/keeper/querier_test.go | 5 ++-- .../08-wasm/light_client_module.go | 4 +-- .../08-wasm/light_client_module_test.go | 27 +++++++++---------- 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/modules/light-clients/08-wasm/keeper/querier.go b/modules/light-clients/08-wasm/keeper/querier.go index 6d932ee57a0..b109af6e64f 100644 --- a/modules/light-clients/08-wasm/keeper/querier.go +++ b/modules/light-clients/08-wasm/keeper/querier.go @@ -45,7 +45,6 @@ type queryHandler struct { } // newQueryHandler returns a default querier that can be used in the contract. -// TODO(jim): Make private and use export_test? func newQueryHandler(ctx sdk.Context, plugins QueryPlugins, callerID string) *queryHandler { return &queryHandler{ Ctx: ctx, diff --git a/modules/light-clients/08-wasm/keeper/querier_test.go b/modules/light-clients/08-wasm/keeper/querier_test.go index 4af9074c12e..f41ee194d0e 100644 --- a/modules/light-clients/08-wasm/keeper/querier_test.go +++ b/modules/light-clients/08-wasm/keeper/querier_test.go @@ -328,13 +328,14 @@ func (suite *KeeperTestSuite) TestStargateQuery() { // NOTE: we register query callbacks against: types.TimestampAtHeightMsg{} // in practise, this can against any client state msg, however registering against types.StatusMsg{} introduces recursive loops // due to test case: "success: verify membership query" - // TODO(Jim): Sanity check that it unmarshals correctly? - _, err = wasmClientKeeper.WasmQuery(suite.chainA.GetContext(), endpoint.ClientID, clientStore, clientState, payload) + res, err := wasmClientKeeper.WasmQuery(suite.chainA.GetContext(), endpoint.ClientID, clientStore, clientState, payload) expPass := tc.expError == nil if expPass { suite.Require().NoError(err) + suite.Require().NotNil(res) } else { + suite.Require().Nil(res) // use error contains as wasmvm errors do not implement errors.Is method suite.Require().ErrorContains(err, tc.expError.Error()) } diff --git a/modules/light-clients/08-wasm/light_client_module.go b/modules/light-clients/08-wasm/light_client_module.go index 7e31029d541..d702ef425d9 100644 --- a/modules/light-clients/08-wasm/light_client_module.go +++ b/modules/light-clients/08-wasm/light_client_module.go @@ -438,13 +438,11 @@ func (l LightClientModule) RecoverClient(ctx sdk.Context, clientID, substituteCl } substituteClientStore := l.storeProvider.ClientStore(ctx, substituteClientID) - substituteClient, found := types.GetClientState(substituteClientStore, cdc) + substituteClientState, found := types.GetClientState(substituteClientStore, cdc) if !found { return errorsmod.Wrap(clienttypes.ErrClientNotFound, substituteClientID) } - substituteClientState := substituteClient - // check that checksums of subject client state and substitute client state match // changing the checksum is only allowed through the migrate contract RPC endpoint if !bytes.Equal(clientState.Checksum, substituteClientState.Checksum) { diff --git a/modules/light-clients/08-wasm/light_client_module_test.go b/modules/light-clients/08-wasm/light_client_module_test.go index 12b75d007a1..ce961fee859 100644 --- a/modules/light-clients/08-wasm/light_client_module_test.go +++ b/modules/light-clients/08-wasm/light_client_module_test.go @@ -854,7 +854,9 @@ func (suite *WasmTestSuite) TestVerifyUpgradeAndUpdateState() { }, clienttypes.ErrClientNotFound, }, - /* TODO(jim): Get back to these. + /* NOTE(jim): This can't fail on unmarshalling, it appears. Any consensus type + we attempt to unmarshal just creates a Wasm ConsensusState that has a + Data field empty. { "failure: upgraded consensus state is not wasm consensus state", func() { @@ -1422,8 +1424,7 @@ func (suite *WasmTestSuite) TestRecoverClient() { bz, err := json.Marshal(types.EmptyResult{}) suite.Require().NoError(err) - // TODO(jim): re-arrange export_test.go contents - prefixedKey := []byte("subject/") + prefixedKey := types.SubjectPrefix prefixedKey = append(prefixedKey, host.ClientStateKey()...) expectedClientStateBz = wasmtesting.CreateMockClientStateBz(suite.chainA.Codec, suite.checksum) store.Set(prefixedKey, expectedClientStateBz) @@ -1462,24 +1463,20 @@ func (suite *WasmTestSuite) TestRecoverClient() { }, clienttypes.ErrClientNotFound, }, - /* TODO(jim): We don't pass a client state directly now so we need to modify how this is tested. - { - "failure: invalid substitute client state", - func() { - substituteClientState = &ibctm.ClientState{} - }, - clienttypes.ErrInvalidClient, - }, { "failure: checksums do not match", func() { - substituteClientState = &types.ClientState{ - Checksum: []byte("invalid"), - } + substituteClientState, found := GetSimApp(suite.chainA).IBCKeeper.ClientKeeper.GetClientState(suite.chainA.GetContext(), substituteClientID) + suite.Require().True(found) + + wasmSubstituteClientState, ok := substituteClientState.(*types.ClientState) + suite.Require().True(ok) + + wasmSubstituteClientState.Checksum = []byte("invalid") + GetSimApp(suite.chainA).IBCKeeper.ClientKeeper.SetClientState(suite.chainA.GetContext(), substituteClientID, wasmSubstituteClientState) }, clienttypes.ErrInvalidClient, }, - */ { "failure: vm returns error", func() { From 8a99c501592b19d840f9fcdd6509b67a33baf228 Mon Sep 17 00:00:00 2001 From: DimitrisJim Date: Thu, 11 Apr 2024 14:18:36 +0300 Subject: [PATCH 4/6] nit: don't hold ref to schema after building it. --- modules/light-clients/08-wasm/keeper/keeper.go | 1 - modules/light-clients/08-wasm/keeper/keeper_vm.go | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/modules/light-clients/08-wasm/keeper/keeper.go b/modules/light-clients/08-wasm/keeper/keeper.go index 70787b21386..586a678fe33 100644 --- a/modules/light-clients/08-wasm/keeper/keeper.go +++ b/modules/light-clients/08-wasm/keeper/keeper.go @@ -31,7 +31,6 @@ type Keeper struct { vm ibcwasm.WasmEngine - schema collections.Schema checksums collections.KeySet[[]byte] storeService store.KVStoreService diff --git a/modules/light-clients/08-wasm/keeper/keeper_vm.go b/modules/light-clients/08-wasm/keeper/keeper_vm.go index 16829c24b64..623733ab246 100644 --- a/modules/light-clients/08-wasm/keeper/keeper_vm.go +++ b/modules/light-clients/08-wasm/keeper/keeper_vm.go @@ -61,14 +61,11 @@ func NewKeeperWithVM( authority: authority, } - schema, err := sb.Build() + _, err := sb.Build() if err != nil { panic(err) } - // TODO(jim): Is this needed? wasmd doesn't hold ref, sdk docs/code do. - keeper.schema = schema - // set query plugins to ensure there is a non-nil query plugin // regardless of what options the user provides keeper.SetQueryPlugins(NewDefaultQueryPlugins(queryRouter)) From fc3aeb5f5aaa78e6ce1a5b525b5c74d4570842d8 Mon Sep 17 00:00:00 2001 From: DimitrisJim Date: Mon, 15 Apr 2024 19:51:44 +0300 Subject: [PATCH 5/6] Update modules/light-clients/08-wasm/types/gas_register.go Co-authored-by: Damian Nolan --- modules/light-clients/08-wasm/types/gas_register.go | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/light-clients/08-wasm/types/gas_register.go b/modules/light-clients/08-wasm/types/gas_register.go index 512df7a8b0f..b8e207cc4ab 100644 --- a/modules/light-clients/08-wasm/types/gas_register.go +++ b/modules/light-clients/08-wasm/types/gas_register.go @@ -68,6 +68,7 @@ var ( Numerator: 15, Denominator: 100, } + VMGasRegister = NewDefaultWasmGasRegister() ) From 715476757e69e0914e2ffae12db48490950dc846 Mon Sep 17 00:00:00 2001 From: DimitrisJim Date: Mon, 15 Apr 2024 20:10:02 +0300 Subject: [PATCH 6/6] lint: shut up --- modules/light-clients/08-wasm/types/gas_register.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/light-clients/08-wasm/types/gas_register.go b/modules/light-clients/08-wasm/types/gas_register.go index b8e207cc4ab..0b16ef6e567 100644 --- a/modules/light-clients/08-wasm/types/gas_register.go +++ b/modules/light-clients/08-wasm/types/gas_register.go @@ -68,7 +68,7 @@ var ( Numerator: 15, Denominator: 100, } - + VMGasRegister = NewDefaultWasmGasRegister() )