mukan-ibc/modules/light-clients/08-wasm/light_client_module.go
Mukan Erkin Törük 88dd97a9f8
Some checks failed
CodeQL / Analyze (push) Waiting to run
golangci-lint / lint (push) Waiting to run
Tests / Code Coverage / build (amd64) (push) Waiting to run
Tests / Code Coverage / build (arm64) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[additional-args:-tags="test_e2e" name:e2e path:./e2e]) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[name:08-wasm path:./modules/light-clients/08-wasm]) (push) Waiting to run
Tests / Code Coverage / unit-tests (map[name:ibc-go path:.]) (push) Waiting to run
Docker Build & Push Simapp (main) / docker-build (push) Has been cancelled
refactor: replace all github.com upstream refs with git.cw.tr/mukan-network
2026-05-11 03:36:22 +03:00

483 lines
18 KiB
Go

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
}