package wasm import ( "bytes" "encoding/hex" "encoding/json" "fmt" errorsmod "cosmossdk.io/errors" sdk "git.cw.tr/mukan-network/mukan-sdk/types" internaltypes "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10/internal/types" wasmkeeper "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10/keeper" "github.com/cosmos/ibc-go/modules/light-clients/08-wasm/v10/types" clienttypes "git.cw.tr/mukan-network/mukan-ibc/modules/core/02-client/types" commitmenttypesv2 "git.cw.tr/mukan-network/mukan-ibc/modules/core/23-commitment/types/v2" ibcerrors "git.cw.tr/mukan-network/mukan-ibc/modules/core/errors" "git.cw.tr/mukan-network/mukan-ibc/modules/core/exported" ) var _ exported.LightClientModule = (*LightClientModule)(nil) // LightClientModule implements the core IBC api.LightClientModule interface. type LightClientModule struct { keeper wasmkeeper.Keeper storeProvider clienttypes.StoreProvider } // NewLightClientModule creates and returns a new 08-wasm LightClientModule. func NewLightClientModule(keeper wasmkeeper.Keeper, storeProvider clienttypes.StoreProvider) LightClientModule { return LightClientModule{ keeper: keeper, storeProvider: storeProvider, } } // 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. func (l LightClientModule) Initialize(ctx sdk.Context, clientID string, clientStateBz, consensusStateBz []byte) error { var clientState types.ClientState if err := l.keeper.Codec().Unmarshal(clientStateBz, &clientState); err != nil { return err } if err := clientState.Validate(); err != nil { return err } var consensusState types.ConsensusState if err := l.keeper.Codec().Unmarshal(consensusStateBz, &consensusState); err != nil { return err } if err := consensusState.ValidateBasic(); err != nil { return err } clientStore := l.storeProvider.ClientStore(ctx, clientID) // 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, 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. func (l LightClientModule) VerifyClientMessage(ctx sdk.Context, clientID string, clientMsg exported.ClientMessage) error { clientStore := l.storeProvider.ClientStore(ctx, clientID) cdc := l.keeper.Codec() clientState, found := types.GetClientState(clientStore, cdc) if !found { return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) } 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, it detects misbehaviour in a submitted Header // message and verifies the correctness of a submitted Misbehaviour ClientMessage. func (l LightClientModule) CheckForMisbehaviour(ctx sdk.Context, clientID string, clientMsg exported.ClientMessage) bool { clientStore := l.storeProvider.ClientStore(ctx, clientID) cdc := l.keeper.Codec() clientState, found := types.GetClientState(clientStore, cdc) if !found { panic(errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID)) } 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 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. func (l LightClientModule) UpdateStateOnMisbehaviour(ctx sdk.Context, clientID string, clientMsg exported.ClientMessage) { clientStore := l.storeProvider.ClientStore(ctx, clientID) cdc := l.keeper.Codec() clientState, found := types.GetClientState(clientStore, cdc) if !found { panic(errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID)) } 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 appropriate // contract endpoint. Client state and new consensus states are updated in the store by the contract. func (l LightClientModule) UpdateState(ctx sdk.Context, clientID string, clientMsg exported.ClientMessage) []exported.Height { clientStore := l.storeProvider.ClientStore(ctx, clientID) cdc := l.keeper.Codec() clientState, found := types.GetClientState(clientStore, cdc) if !found { panic(errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID)) } 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 := make([]exported.Height, 0, len(result.Heights)) 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 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. func (l LightClientModule) VerifyMembership( ctx sdk.Context, clientID string, height exported.Height, delayTimePeriod uint64, delayBlockPeriod uint64, proof []byte, path exported.Path, value []byte, ) error { clientStore := l.storeProvider.ClientStore(ctx, clientID) cdc := l.keeper.Codec() clientState, found := types.GetClientState(clientStore, cdc) if !found { return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) } 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.(commitmenttypesv2.MerklePath) if !ok { return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", commitmenttypesv2.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 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. func (l LightClientModule) VerifyNonMembership( ctx sdk.Context, clientID string, height exported.Height, delayTimePeriod uint64, delayBlockPeriod uint64, proof []byte, path exported.Path, ) error { clientStore := l.storeProvider.ClientStore(ctx, clientID) cdc := l.keeper.Codec() clientState, found := types.GetClientState(clientStore, cdc) if !found { return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) } 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.(commitmenttypesv2.MerklePath) if !ok { return errorsmod.Wrapf(ibcerrors.ErrInvalidType, "expected %T, got %T", commitmenttypesv2.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 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. func (l LightClientModule) Status(ctx sdk.Context, clientID string) exported.Status { clientStore := l.storeProvider.ClientStore(ctx, clientID) cdc := l.keeper.Codec() clientState, found := types.GetClientState(clientStore, cdc) if !found { return exported.Unknown } // 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. // If no client is present for the provided client identifier a zero value height is returned. func (l LightClientModule) LatestHeight(ctx sdk.Context, clientID string) exported.Height { clientStore := l.storeProvider.ClientStore(ctx, clientID) cdc := l.keeper.Codec() clientState, found := types.GetClientState(clientStore, cdc) if !found { return clienttypes.ZeroHeight() } return clientState.LatestHeight } // 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. func (l LightClientModule) TimestampAtHeight(ctx sdk.Context, clientID string, height exported.Height) (uint64, error) { clientStore := l.storeProvider.ClientStore(ctx, clientID) cdc := l.keeper.Codec() clientState, found := types.GetClientState(clientStore, cdc) if !found { return 0, errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) } 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 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. func (l LightClientModule) RecoverClient(ctx sdk.Context, clientID, substituteClientID string) error { substituteClientType, _, err := clienttypes.ParseClientIdentifier(substituteClientID) if err != nil { return err } if substituteClientType != types.Wasm { return errorsmod.Wrapf(clienttypes.ErrInvalidClientType, "expected: %s, got: %s", types.Wasm, substituteClientType) } cdc := l.keeper.Codec() subjectClientStore := l.storeProvider.ClientStore(ctx, clientID) subjectClientState, found := types.GetClientState(subjectClientStore, cdc) if !found { return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) } substituteClientStore := l.storeProvider.ClientStore(ctx, substituteClientID) substituteClientState, found := types.GetClientState(substituteClientStore, cdc) if !found { return errorsmod.Wrap(clienttypes.ErrClientNotFound, substituteClientID) } // 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(subjectClientState.Checksum, substituteClientState.Checksum) { return errorsmod.Wrapf(clienttypes.ErrInvalidClient, "expected checksums to be equal: expected %s, got %s", hex.EncodeToString(subjectClientState.Checksum), hex.EncodeToString(substituteClientState.Checksum)) } store := internaltypes.NewClientRecoveryStore(subjectClientStore, substituteClientStore) payload := types.SudoMsg{ MigrateClientStore: &types.MigrateClientStoreMsg{}, } _, err = l.keeper.WasmSudo(ctx, clientID, store, subjectClientState, payload) return err } // 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. On a successful verification, it expects the contract to update the new client state, consensus state, and any other client metadata. func (l LightClientModule) VerifyUpgradeAndUpdateState( ctx sdk.Context, clientID string, newClient []byte, newConsState []byte, upgradeClientProof, upgradeConsensusStateProof []byte, ) error { cdc := l.keeper.Codec() var newClientState types.ClientState if err := cdc.Unmarshal(newClient, &newClientState); err != nil { return errorsmod.Wrap(clienttypes.ErrInvalidClient, err.Error()) } var newConsensusState types.ConsensusState if err := cdc.Unmarshal(newConsState, &newConsensusState); err != nil { return errorsmod.Wrap(clienttypes.ErrInvalidConsensus, err.Error()) } clientStore := l.storeProvider.ClientStore(ctx, clientID) clientState, found := types.GetClientState(clientStore, cdc) if !found { return errorsmod.Wrap(clienttypes.ErrClientNotFound, clientID) } // last height of current counterparty chain must be client's latest height lastHeight := clientState.LatestHeight if !newClientState.LatestHeight.GT(lastHeight) { return errorsmod.Wrapf(ibcerrors.ErrInvalidHeight, "upgraded client height %s must be at greater than current client height %s", newClientState.LatestHeight, lastHeight) } 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 }