diff --git a/ibc/client/events.go b/ibc/client/events.go new file mode 100644 index 000000000..7ea984c5b --- /dev/null +++ b/ibc/client/events.go @@ -0,0 +1,74 @@ +package client + +import ( + client_types "github.com/pokt-network/pocket/ibc/client/types" + "github.com/pokt-network/pocket/shared/codec" + core_types "github.com/pokt-network/pocket/shared/core/types" + "github.com/pokt-network/pocket/shared/modules" +) + +// emitCreateClientEvent emits a create client event +func (c *clientManager) emitCreateClientEvent(clientId string, clientState modules.ClientState) error { + return c.GetBus().GetEventLogger().EmitEvent( + &core_types.IBCEvent{ + Topic: client_types.EventTopicCreateClient, + Attributes: []*core_types.Attribute{ + core_types.NewAttribute(client_types.AttributeKeyClientID, []byte(clientId)), + core_types.NewAttribute(client_types.AttributeKeyClientType, []byte(clientState.ClientType())), + core_types.NewAttribute(client_types.AttributeKeyConsensusHeight, []byte(clientState.GetLatestHeight().ToString())), + }, + }, + ) +} + +// emitUpdateClientEvent emits an update client event +func (c *clientManager) emitUpdateClientEvent( + clientId, clientType string, + consensusHeight modules.Height, + clientMessage modules.ClientMessage, +) error { + // Marshall the client message + clientMsgBz, err := codec.GetCodec().Marshal(clientMessage) + if err != nil { + return err + } + + return c.GetBus().GetEventLogger().EmitEvent( + &core_types.IBCEvent{ + Topic: client_types.EventTopicUpdateClient, + Attributes: []*core_types.Attribute{ + core_types.NewAttribute(client_types.AttributeKeyClientID, []byte(clientId)), + core_types.NewAttribute(client_types.AttributeKeyClientType, []byte(clientType)), + core_types.NewAttribute(client_types.AttributeKeyConsensusHeight, []byte(consensusHeight.ToString())), + core_types.NewAttribute(client_types.AttributeKeyHeader, clientMsgBz), + }, + }, + ) +} + +// emitUpgradeClientEvent emits an upgrade client event +func (c *clientManager) emitUpgradeClientEvent(clientId string, clientState modules.ClientState) error { + return c.GetBus().GetEventLogger().EmitEvent( + &core_types.IBCEvent{ + Topic: client_types.EventTopicUpdateClient, + Attributes: []*core_types.Attribute{ + core_types.NewAttribute(client_types.AttributeKeyClientID, []byte(clientId)), + core_types.NewAttribute(client_types.AttributeKeyClientType, []byte(clientState.ClientType())), + core_types.NewAttribute(client_types.AttributeKeyConsensusHeight, []byte(clientState.GetLatestHeight().ToString())), + }, + }, + ) +} + +// emitSubmitMisbehaviourEvent emits a submit misbehaviour event +func (c *clientManager) emitSubmitMisbehaviourEvent(clientId string, clientState modules.ClientState) error { + return c.GetBus().GetEventLogger().EmitEvent( + &core_types.IBCEvent{ + Topic: client_types.EventTopicSubmitMisbehaviour, + Attributes: []*core_types.Attribute{ + core_types.NewAttribute(client_types.AttributeKeyClientID, []byte(clientId)), + core_types.NewAttribute(client_types.AttributeKeyClientType, []byte(clientState.ClientType())), + }, + }, + ) +} diff --git a/ibc/client/introspect.go b/ibc/client/introspect.go new file mode 100644 index 000000000..80b0bf2b8 --- /dev/null +++ b/ibc/client/introspect.go @@ -0,0 +1,152 @@ +package client + +import ( + "errors" + "time" + + light_client_types "github.com/pokt-network/pocket/ibc/client/light_clients/types" + "github.com/pokt-network/pocket/ibc/client/types" + ibc_types "github.com/pokt-network/pocket/ibc/types" + "github.com/pokt-network/pocket/shared/codec" + "github.com/pokt-network/pocket/shared/modules" + util_types "github.com/pokt-network/pocket/utility/types" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" +) + +// GetHostConsensusState returns the ConsensusState at the given height for the +// host chain, the Pocket network. It then serialises this and packs it into a +// ConsensusState object for use in a WASM client +func (c *clientManager) GetHostConsensusState(height modules.Height) (modules.ConsensusState, error) { + blockStore := c.GetBus().GetPersistenceModule().GetBlockStore() + block, err := blockStore.GetBlock(height.GetRevisionHeight()) + if err != nil { + return nil, err + } + pocketConsState := &light_client_types.PocketConsensusState{ + Timestamp: block.BlockHeader.Timestamp, + StateHash: block.BlockHeader.StateHash, + StateTreeHashes: block.BlockHeader.StateTreeHashes, + NextValSetHash: block.BlockHeader.NextValSetHash, + } + consBz, err := codec.GetCodec().Marshal(pocketConsState) + if err != nil { + return nil, err + } + return types.NewConsensusState(consBz, uint64(pocketConsState.Timestamp.AsTime().UnixNano())), nil +} + +// GetHostClientState returns the ClientState at the given height for the host +// chain, the Pocket network. +// +// This function is used to validate the state of a client running on a +// counterparty chain. +func (c *clientManager) GetHostClientState(height modules.Height) (modules.ClientState, error) { + blockStore := c.GetBus().GetPersistenceModule().GetBlockStore() + block, err := blockStore.GetBlock(height.GetRevisionHeight()) + if err != nil { + return nil, err + } + rCtx, err := c.GetBus().GetPersistenceModule().NewReadContext(int64(height.GetRevisionHeight())) + if err != nil { + return nil, err + } + defer rCtx.Release() + unbondingBlocks, err := rCtx.GetIntParam(util_types.ValidatorUnstakingBlocksParamName, int64(height.GetRevisionHeight())) + if err != nil { + return nil, err + } + // TODO_AFTER(#705): use the actual MinimumBlockTime once set + blockTime := time.Minute * 15 + unbondingPeriod := blockTime * time.Duration(unbondingBlocks) // approx minutes per block * blocks + pocketClient := &light_client_types.PocketClientState{ + NetworkId: block.BlockHeader.NetworkId, + TrustLevel: &light_client_types.Fraction{Numerator: 2, Denominator: 3}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod), + MaxClockDrift: durationpb.New(blockTime), // DISCUSS: What is a reasonable MaxClockDrift? + LatestHeight: &types.Height{ + RevisionNumber: height.GetRevisionNumber(), + RevisionHeight: height.GetRevisionHeight(), + }, + ProofSpec: ibc_types.SmtSpec, + } + clientBz, err := codec.GetCodec().Marshal(pocketClient) + if err != nil { + return nil, err + } + return &types.ClientState{ + Data: clientBz, + RecentHeight: pocketClient.LatestHeight, + }, nil +} + +// VerifyHostClientState verifies that a ClientState for a light client running +// on a counterparty chain is valid, by checking it against the result of +// GetHostClientState(counterpartyClientState.GetLatestHeight()) +func (c *clientManager) VerifyHostClientState(counterparty modules.ClientState) error { + height, err := c.GetCurrentHeight() + if err != nil { + return err + } + hostState, err := c.GetHostClientState(height) + if err != nil { + return err + } + poktHost := new(light_client_types.PocketClientState) + err = codec.GetCodec().Unmarshal(hostState.GetData(), poktHost) + if err != nil { + return err + } + poktCounter := new(light_client_types.PocketClientState) + err = codec.GetCodec().Unmarshal(counterparty.GetData(), poktCounter) + if err != nil { + return errors.New("counterparty client state is not a PocketClientState") + } + + if poktCounter.FrozenHeight > 0 { + return errors.New("counterparty client state is frozen") + } + if poktCounter.NetworkId != poktHost.NetworkId { + return errors.New("counterparty client state has different network id") + } + if poktCounter.LatestHeight.RevisionNumber != poktHost.LatestHeight.RevisionNumber { + return errors.New("counterparty client state has different revision number") + } + if poktCounter.GetLatestHeight().GTE(poktHost.GetLatestHeight()) { + return errors.New("counterparty client state has a height greater than or equal to the host client state") + } + if poktCounter.TrustLevel.LT(&light_client_types.Fraction{Numerator: 2, Denominator: 3}) || + poktCounter.TrustLevel.GT(&light_client_types.Fraction{Numerator: 1, Denominator: 1}) { + return errors.New("counterparty client state trust level is not in the accepted range") + } + if !proto.Equal(poktCounter.ProofSpec, poktHost.ProofSpec) { + return errors.New("counterparty client state has different proof spec") + } + if poktCounter.UnbondingPeriod != poktHost.UnbondingPeriod { + return errors.New("counterparty client state has different unbonding period") + } + if poktCounter.UnbondingPeriod.AsDuration().Nanoseconds() < poktHost.TrustingPeriod.AsDuration().Nanoseconds() { + return errors.New("counterparty client state unbonding period is less than trusting period") + } + + // RESEARCH: Look into upgrade paths, their use and if they should just be equal + + return nil +} + +// GetCurrentHeight returns the current IBC client height of the network +// TODO_AFTER(#882): Use actual revision number +func (h *clientManager) GetCurrentHeight() (modules.Height, error) { + currHeight := h.GetBus().GetConsensusModule().CurrentHeight() + rCtx, err := h.GetBus().GetPersistenceModule().NewReadContext(int64(currHeight)) + if err != nil { + return nil, err + } + defer rCtx.Release() + revNum := rCtx.GetRevisionNumber(int64(currHeight)) + return &types.Height{ + RevisionNumber: revNum, + RevisionHeight: currHeight, + }, nil +} diff --git a/ibc/client/light_clients/types/fraction.go b/ibc/client/light_clients/types/fraction.go new file mode 100644 index 000000000..75a2471ab --- /dev/null +++ b/ibc/client/light_clients/types/fraction.go @@ -0,0 +1,42 @@ +package types + +type ord int + +const ( + lt ord = iota + eq + gt +) + +func (f *Fraction) LT(other *Fraction) bool { + return f.compare(other) == lt +} + +func (f *Fraction) GT(other *Fraction) bool { + return f.compare(other) == gt +} + +func (f *Fraction) EQ(other *Fraction) bool { + return f.compare(other) == eq +} + +func (f *Fraction) LTE(other *Fraction) bool { + return f.compare(other) != gt +} + +func (f *Fraction) GTE(other *Fraction) bool { + return f.compare(other) != lt +} + +func (f *Fraction) compare(other *Fraction) ord { + comDenom := f.Denominator * other.Denominator + aNum := f.Numerator * (comDenom / f.Denominator) + bNum := other.Numerator * (comDenom / other.Denominator) + if aNum < bNum { + return lt + } + if aNum > bNum { + return gt + } + return eq +} diff --git a/ibc/client/queries.go b/ibc/client/queries.go new file mode 100644 index 000000000..2ea5b36dc --- /dev/null +++ b/ibc/client/queries.go @@ -0,0 +1,34 @@ +package client + +import ( + "github.com/pokt-network/pocket/ibc/client/types" + "github.com/pokt-network/pocket/ibc/path" + core_types "github.com/pokt-network/pocket/shared/core/types" + "github.com/pokt-network/pocket/shared/modules" +) + +// GetConsensusState returns the ConsensusState at the given height for the +// stored client with the given identifier +func (c *clientManager) GetConsensusState( + identifier string, height modules.Height, +) (modules.ConsensusState, error) { + // Retrieve the clientId prefixed client store + prefixed := path.ApplyPrefix(core_types.CommitmentPrefix(path.KeyClientStorePrefix), identifier) + clientStore, err := c.GetBus().GetIBCHost().GetProvableStore(string(prefixed)) + if err != nil { + return nil, err + } + + return types.GetConsensusState(clientStore, height) +} + +// GetClientState returns the ClientState for the stored client with the given identifier +func (c *clientManager) GetClientState(identifier string) (modules.ClientState, error) { + // Retrieve the client store + clientStore, err := c.GetBus().GetIBCHost().GetProvableStore(path.KeyClientStorePrefix) + if err != nil { + return nil, err + } + + return types.GetClientState(clientStore, identifier) +} diff --git a/ibc/client/submodule.go b/ibc/client/submodule.go new file mode 100644 index 000000000..8428f8ecd --- /dev/null +++ b/ibc/client/submodule.go @@ -0,0 +1,225 @@ +package client + +import ( + "fmt" + + "github.com/pokt-network/pocket/ibc/client/types" + "github.com/pokt-network/pocket/ibc/path" + core_types "github.com/pokt-network/pocket/shared/core/types" + "github.com/pokt-network/pocket/shared/modules" + "github.com/pokt-network/pocket/shared/modules/base_modules" +) + +var ( + _ modules.ClientManager = &clientManager{} + allowedClientTypes = make(map[string]struct{}, 0) +) + +func init() { + allowedClientTypes[types.WasmClientType] = struct{}{} +} + +type clientManager struct { + base_modules.IntegrableModule + + logger *modules.Logger +} + +func Create(bus modules.Bus, options ...modules.ClientManagerOption) (modules.ClientManager, error) { + return new(clientManager).Create(bus, options...) +} + +// WithLogger sets the logger for the clientManager +func WithLogger(logger *modules.Logger) modules.ClientManagerOption { + return func(m modules.ClientManager) { + if mod, ok := m.(*clientManager); ok { + mod.logger = logger + } + } +} + +func (*clientManager) Create(bus modules.Bus, options ...modules.ClientManagerOption) (modules.ClientManager, error) { + c := &clientManager{} + + for _, option := range options { + option(c) + } + + c.logger.Info().Msg("👨 Creating Client Manager 👨") + + bus.RegisterModule(c) + + return c, nil +} + +func (c *clientManager) GetModuleName() string { return modules.ClientManagerModuleName } + +// CreateClient creates a new client with the given client state and initial +// consensus state and initialises it with a unique identifier in the IBC client +// store and emits an event to the Event Logger +func (c *clientManager) CreateClient( + clientState modules.ClientState, consensusState modules.ConsensusState, +) (string, error) { + // Check if the client type is allowed + if !isAllowedClientType(clientState.ClientType()) { + return "", fmt.Errorf("client type %s is not supported", clientState.ClientType()) + } + + // Generate a unique identifier for the client + identifier := path.GenerateClientIdentifier() + + // Retrieve the client store prefixed with the client identifier + prefixed := path.ApplyPrefix(core_types.CommitmentPrefix(path.KeyClientStorePrefix), identifier) + clientStore, err := c.GetBus().GetIBCHost().GetProvableStore(string(prefixed)) + if err != nil { + return "", err + } + + // Initialise the client with the clientState provided + if err := clientState.Initialise(clientStore, consensusState); err != nil { + c.logger.Error().Err(err).Str("identifier", identifier). + Msg("failed to initialize client") + return "", err + } + + c.logger.Info().Str("identifier", identifier). + Str("height", clientState.GetLatestHeight().ToString()). + Msg("client created at height") + + // Emit the create client event to the event logger + if err := c.emitCreateClientEvent(identifier, clientState); err != nil { + c.logger.Error().Err(err).Str("identifier", identifier). + Msg("failed to emit client created event") + return "", err + } + + return identifier, nil +} + +// UpdateClient updates an existing client with the given identifier using the +// ClientMessage provided +func (c *clientManager) UpdateClient( + identifier string, clientMessage modules.ClientMessage, +) error { + // Get the client state + clientState, err := c.GetClientState(identifier) + if err != nil { + return err + } + + // Retrieve the client store prefixed with the client identifier + prefixed := path.ApplyPrefix(core_types.CommitmentPrefix(path.KeyClientStorePrefix), identifier) + clientStore, err := c.GetBus().GetIBCHost().GetProvableStore(string(prefixed)) + if err != nil { + return err + } + + // Check the state is active + if clientState.Status(clientStore) != modules.ActiveStatus { + return core_types.ErrIBCClientNotActive() + } + + // Verify the client message + if err := clientState.VerifyClientMessage(clientStore, clientMessage); err != nil { + return err + } + + // Check for misbehaviour on the source chain + misbehaved := clientState.CheckForMisbehaviour(clientStore, clientMessage) + if misbehaved { + if err := clientState.UpdateStateOnMisbehaviour(clientStore, clientMessage); err != nil { + c.logger.Error().Err(err).Str("identifier", identifier). + Msg("failed to freeze client for misbehaviour") + return err + } + c.logger.Info().Str("identifier", identifier). + Msg("client frozen for misbehaviour") + + // emit the submit misbehaviour event to the event logger + if err := c.emitSubmitMisbehaviourEvent(identifier, clientState); err != nil { + c.logger.Error().Err(err).Str("identifier", identifier). + Msg("failed to emit client submit misbehaviour event") + return err + } + return nil + } + + // Update the client + consensusHeight, err := clientState.UpdateState(clientStore, clientMessage) + if err != nil { + c.logger.Error().Err(err).Str("identifier", identifier). + Str("height", consensusHeight.ToString()). + Msg("failed to update client state") + return err + } + c.logger.Info().Str("identifier", identifier). + Str("height", consensusHeight.ToString()). + Msg("client state updated") + + // emit the update client event to the event logger + if err := c.emitUpdateClientEvent(identifier, clientState.ClientType(), consensusHeight, clientMessage); err != nil { + c.logger.Error().Err(err).Str("identifier", identifier). + Msg("failed to emit client update event") + return err + } + + return nil +} + +// UpgradeClient upgrades an existing client with the given identifier using the +// ClientState and ConsentusState provided. It can only do so if the new client +// was committed to by the old client at the specified upgrade height +func (c *clientManager) UpgradeClient( + identifier string, + upgradedClient modules.ClientState, upgradedConsState modules.ConsensusState, + proofUpgradeClient, proofUpgradeConsState []byte, +) error { + // Get the client state + clientState, err := c.GetClientState(identifier) + if err != nil { + return err + } + + // Retrieve the client store prefixed with the client identifier + prefixed := path.ApplyPrefix(core_types.CommitmentPrefix(path.KeyClientStorePrefix), identifier) + clientStore, err := c.GetBus().GetIBCHost().GetProvableStore(string(prefixed)) + if err != nil { + return err + } + + // Check the state is active + if clientState.Status(clientStore) != modules.ActiveStatus { + return core_types.ErrIBCClientNotActive() + } + + // Verify the upgrade + if err := clientState.VerifyUpgradeAndUpdateState( + clientStore, + upgradedClient, upgradedConsState, + proofUpgradeClient, proofUpgradeConsState, + ); err != nil { + c.logger.Error().Err(err).Str("identifier", identifier). + Msg("failed to verify upgrade") + return err + } + + c.logger.Info().Str("identifier", identifier). + Str("height", upgradedClient.GetLatestHeight().ToString()). + Msg("client upgraded") + + // emit the upgrade client event to the event logger + if err := c.emitUpgradeClientEvent(identifier, upgradedClient); err != nil { + c.logger.Error().Err(err).Str("identifier", identifier). + Msg("failed to emit client upgrade event") + return err + } + + return nil +} + +func isAllowedClientType(clientType string) bool { + if _, ok := allowedClientTypes[clientType]; ok { + return true + } + return false +} diff --git a/ibc/client/types/client.go b/ibc/client/types/client.go new file mode 100644 index 000000000..e438eedfb --- /dev/null +++ b/ibc/client/types/client.go @@ -0,0 +1,242 @@ +package types + +import ( + "errors" + "fmt" + + core_types "github.com/pokt-network/pocket/shared/core/types" + "github.com/pokt-network/pocket/shared/modules" +) + +const ( + // https://github.com/cosmos/ibc/blob/main/spec/client/ics-008-wasm-client/README.md + WasmClientType = "08-wasm" +) + +var _ modules.ClientState = &ClientState{} + +// ClientType returns the client type. +func (cs *ClientState) ClientType() string { return WasmClientType } + +// GetLatestHeight returns the latest height stored. +func (cs *ClientState) GetLatestHeight() modules.Height { return cs.RecentHeight } + +// Validate performs a basic validation of the client state fields. +func (cs *ClientState) Validate() error { + if len(cs.Data) == 0 { + return errors.New("data cannot be empty") + } + + lenWasmChecksum := len(cs.WasmChecksum) + if lenWasmChecksum == 0 { + return errors.New("wasm checksum cannot be empty") + } + if lenWasmChecksum != 32 { // sha256 output is 256 bits long + return fmt.Errorf("expected 32, got %d", lenWasmChecksum) + } + + return nil +} + +//nolint:unused // types defined for future use +type ( + statusInnerPayload struct{} + statusPayload struct { + Status statusInnerPayload `json:"status"` + } +) + +// 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(clientStore modules.ProvableStore) modules.ClientStatus { + /* + payload := &statusPayload{Status: statusInnerPayload{}} + encodedData, err := json.Marshal(payload) + if err != nil { + return modules.UnknownStatus + } + + // TODO(#912): implement WASM contract querying + */ + return modules.ActiveStatus +} + +// GetTimestampAtHeight returns the timestamp of the consensus state at the given height. +func (cs *ClientState) GetTimestampAtHeight(clientStore modules.ProvableStore, height modules.Height) (uint64, error) { + consState, err := GetConsensusState(clientStore, height) + if err != nil { + return 0, err + } + return consState.GetTimestamp(), nil +} + +// Initialise 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) Initialise(clientStore modules.ProvableStore, consensusState modules.ConsensusState) error { + consState, ok := consensusState.(*ConsensusState) + if !ok { + return errors.New("invalid consensus state type") + } + if err := setClientState(clientStore, cs); err != nil { + return fmt.Errorf("failed to set client state: %w", err) + } + if err := setConsensusState(clientStore, consState, cs.GetLatestHeight()); err != nil { + return fmt.Errorf("failed to set consensus state: %w", err) + } + // TODO(#912): implement WASM contract initialisation + return nil +} + +//nolint:unused // types defined for future use +type ( + verifyMembershipInnerPayload struct { + Height modules.Height `json:"height"` + DelayTimePeriod uint64 `json:"delay_time_period"` + DelayBlockPeriod uint64 `json:"delay_block_period"` + Proof []byte `json:"proof"` + Path core_types.CommitmentPath `json:"path"` + Value []byte `json:"value"` + } + verifyMembershipPayload struct { + VerifyMembership verifyMembershipInnerPayload `json:"verify_membership"` + } +) + +// 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( + clientStore modules.ProvableStore, + height modules.Height, + delayTimePeriod, delayBlockPeriod uint64, + proof, key, value []byte, +) error { + if cs.GetLatestHeight().LT(height) { + return fmt.Errorf("client state height < proof height (%d < %d)", cs.GetLatestHeight(), height) + } + + if _, err := GetConsensusState(clientStore, height); err != nil { + return errors.New("consensus state not found for proof height") + } + + /* + payload := verifyMembershipPayload{ + VerifyMembership: verifyMembershipInnerPayload{ + Height: height, + DelayTimePeriod: delayTimePeriod, + DelayBlockPeriod: delayBlockPeriod, + Proof: proof, + Path: key, + Value: value, + }, + } + + // TODO(#912): implement WASM contract method calls + */ + + return nil +} + +//nolint:unused // types defined for future use +type ( + verifyNonMembershipInnerPayload struct { + Height modules.Height `json:"height"` + DelayTimePeriod uint64 `json:"delay_time_period"` + DelayBlockPeriod uint64 `json:"delay_block_period"` + Proof []byte `json:"proof"` + Path core_types.CommitmentPath `json:"path"` + } + verifyNonMembershipPayload struct { + VerifyNonMembership verifyNonMembershipInnerPayload `json:"verify_non_membership"` + } +) + +// 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( + clientStore modules.ProvableStore, + height modules.Height, + delayTimePeriod, delayBlockPeriod uint64, + proof, key []byte, +) error { + if cs.GetLatestHeight().LT(height) { + return fmt.Errorf("client state height < proof height (%d < %d)", cs.GetLatestHeight(), height) + } + + if _, err := GetConsensusState(clientStore, height); err != nil { + return errors.New("consensus state not found for proof height") + } + + /* + payload := verifyNonMembershipPayload{ + VerifyNonMembership: verifyNonMembershipInnerPayload{ + Height: height, + DelayTimePeriod: delayTimePeriod, + DelayBlockPeriod: delayBlockPeriod, + Proof: proof, + Path: key, + }, + } + + // TODO(#912): implement WASM contract method calls + */ + + return nil +} + +//nolint:unused // types defined for future use +type ( + checkForMisbehaviourInnerPayload struct { + ClientMessage clientMessage `json:"client_message"` + } + checkForMisbehaviourPayload struct { + CheckForMisbehaviour checkForMisbehaviourInnerPayload `json:"check_for_misbehaviour"` + } +) + +// CheckForMisbehaviour detects misbehaviour in a submitted Header message and +// verifies the correctness of a submitted Misbehaviour ClientMessage +func (cs *ClientState) CheckForMisbehaviour(clientStore modules.ProvableStore, clientMsg modules.ClientMessage) bool { + clientMsgConcrete := clientMessage{ + Header: nil, + Misbehaviour: nil, + } + switch msg := clientMsg.(type) { + case *Header: + clientMsgConcrete.Header = msg + case *Misbehaviour: + clientMsgConcrete.Misbehaviour = msg + } + + if clientMsgConcrete.Header == nil && clientMsgConcrete.Misbehaviour == nil { + return false + } + + /* + inner := checkForMisbehaviourInnerPayload{ + ClientMessage: clientMsgConcrete, + } + payload := checkForMisbehaviourPayload{ + CheckForMisbehaviour: inner, + } + + // TODO(#912): implement WASM contract method calls + */ + + return true +} diff --git a/ibc/client/types/consensus.go b/ibc/client/types/consensus.go new file mode 100644 index 000000000..5e089cbd5 --- /dev/null +++ b/ibc/client/types/consensus.go @@ -0,0 +1,33 @@ +package types + +import ( + "errors" + + "github.com/pokt-network/pocket/shared/modules" +) + +var _ modules.ConsensusState = &ConsensusState{} + +// NewConsensusState creates a new ConsensusState instance. +func NewConsensusState(data []byte, timestamp uint64) *ConsensusState { + return &ConsensusState{ + Data: data, + Timestamp: timestamp, + } +} + +// ClientType returns the Wasm client type. +func (cs *ConsensusState) ClientType() string { + return WasmClientType +} + +// ValidateBasic defines a basic validation for the wasm client consensus state. +func (cs *ConsensusState) ValidateBasic() error { + if cs.Timestamp == 0 { + return errors.New("timestamp must be a positive Unix time") + } + if len(cs.Data) == 0 { + return errors.New("data cannot be empty") + } + return nil +} diff --git a/ibc/client/types/events.go b/ibc/client/types/events.go new file mode 100644 index 000000000..0900003c3 --- /dev/null +++ b/ibc/client/types/events.go @@ -0,0 +1,17 @@ +package types + +const ( + // Event topics for the events emitted by the Client submodule + EventTopicCreateClient = "create_client" + EventTopicUpdateClient = "update_client" + EventTopicUpgradeClient = "upgrade_client" + EventTopicSubmitMisbehaviour = "client_misbehaviour" +) + +var ( + // Attribute keys for the events emitted by the Client submodule + AttributeKeyClientID = []byte("client_id") + AttributeKeyClientType = []byte("client_type") + AttributeKeyConsensusHeight = []byte("consensus_height") + AttributeKeyHeader = []byte("header") +) diff --git a/ibc/client/types/header.go b/ibc/client/types/header.go new file mode 100644 index 000000000..1522c49db --- /dev/null +++ b/ibc/client/types/header.go @@ -0,0 +1,23 @@ +package types + +import ( + "errors" + + "github.com/pokt-network/pocket/shared/modules" +) + +var _ modules.ClientMessage = &Header{} + +// ClientType defines that the Header is a Wasm client consensus algorithm +func (h *Header) ClientType() string { + return WasmClientType +} + +// ValidateBasic defines a basic validation for the wasm client header. +func (h *Header) ValidateBasic() error { + if len(h.Data) == 0 { + return errors.New("data cannot be empty") + } + + return nil +} diff --git a/ibc/client/types/height.go b/ibc/client/types/height.go new file mode 100644 index 000000000..2c57bc359 --- /dev/null +++ b/ibc/client/types/height.go @@ -0,0 +1,76 @@ +package types + +import ( + "fmt" + + "github.com/pokt-network/pocket/shared/modules" +) + +type ord int + +const ( + lt ord = iota - 1 + eq + gt +) + +func (h *Height) ToString() string { + return fmt.Sprintf("%d-%d", h.RevisionNumber, h.RevisionHeight) +} + +func (h *Height) IsZero() bool { + return h.RevisionNumber == 0 && h.RevisionHeight == 0 +} + +func (h *Height) LT(other modules.Height) bool { + return h.compare(other) == lt +} + +func (h *Height) LTE(other modules.Height) bool { + return h.compare(other) != gt +} + +func (h *Height) GT(other modules.Height) bool { + return h.compare(other) == gt +} + +func (h *Height) GTE(other modules.Height) bool { + return h.compare(other) != lt +} + +func (h *Height) EQ(other modules.Height) bool { + return h.compare(other) == eq +} + +func (h *Height) Increment() modules.Height { + return &Height{ + RevisionNumber: h.RevisionNumber, + RevisionHeight: h.RevisionHeight + 1, + } +} + +func (h *Height) Decrement() modules.Height { + if h.RevisionHeight == 0 { + return h + } + return &Height{ + RevisionNumber: h.RevisionNumber, + RevisionHeight: h.RevisionHeight - 1, + } +} + +func (h *Height) compare(other modules.Height) ord { + if h.RevisionNumber > other.GetRevisionNumber() { + return gt + } + if h.RevisionNumber < other.GetRevisionNumber() { + return lt + } + if h.RevisionHeight > other.GetRevisionHeight() { + return gt + } + if h.RevisionHeight < other.GetRevisionHeight() { + return lt + } + return eq +} diff --git a/ibc/client/types/height_test.go b/ibc/client/types/height_test.go new file mode 100644 index 000000000..4b8c34eb8 --- /dev/null +++ b/ibc/client/types/height_test.go @@ -0,0 +1,430 @@ +package types + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func FuzzHeight_ToStringDeterministic(f *testing.F) { + for i := 0; i < 100; i++ { + f.Add(uint64(i)) + } + f.Fuzz(func(t *testing.T, i uint64) { + height := &Height{ + RevisionNumber: i, + RevisionHeight: i, + } + str := height.ToString() + require.Equal(t, str, fmt.Sprintf("%d-%d", i, i)) + }) +} + +func TestHeight_IsZero(t *testing.T) { + testCases := []struct { + name string + height *Height + expected bool + }{ + { + name: "zero height", + height: &Height{ + RevisionNumber: 0, + RevisionHeight: 0, + }, + expected: true, + }, + { + name: "non-zero height: zero revision number", + height: &Height{ + RevisionNumber: 0, + RevisionHeight: 1, + }, + expected: false, + }, + { + name: "non-zero height: zero revision height", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 0, + }, + expected: false, + }, + { + name: "non-zero height", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expected, tc.height.IsZero()) + }) + } +} + +func TestHeight_Increment(t *testing.T) { + height := &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + } + newHeight := height.Increment() + require.Equal(t, uint64(1), height.GetRevisionNumber()) + require.Equal(t, uint64(2), newHeight.GetRevisionHeight()) + + newHeight = newHeight.Increment() + require.Equal(t, uint64(1), height.GetRevisionNumber()) + require.Equal(t, uint64(3), newHeight.GetRevisionHeight()) +} + +func TestHeight_Decrement(t *testing.T) { + height := &Height{ + RevisionNumber: 1, + RevisionHeight: 2, + } + newHeight := height.Decrement() + require.Equal(t, uint64(1), height.GetRevisionNumber()) + require.Equal(t, uint64(1), newHeight.GetRevisionHeight()) + + newHeight = newHeight.Decrement() + require.Equal(t, uint64(1), height.GetRevisionNumber()) + require.Equal(t, uint64(0), newHeight.GetRevisionHeight()) + + newHeight = newHeight.Decrement() + require.Equal(t, uint64(1), height.GetRevisionNumber()) + require.Equal(t, uint64(0), newHeight.GetRevisionHeight()) +} + +func TestHeight_Comparisons(t *testing.T) { + testCases := []struct { + name string + op string + height *Height + other *Height + expected bool + }{ + { + name: "LT: height < other", + op: "LT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + expected: true, + }, + { + name: "LT: height == other", + op: "LT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: false, + }, + { + name: "LT: height > other", + op: "LT", + height: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: false, + }, + { + name: "LT: height < other (same revision number)", + op: "LT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 2, + }, + expected: true, + }, + { + name: "LT: height > other (same revision number)", + op: "LT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 2, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: false, + }, + { + name: "LT: height > other (same revision height)", + op: "LT", + height: &Height{ + RevisionNumber: 2, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: false, + }, + { + name: "LT: height < other (same revision height)", + op: "LT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 2, + RevisionHeight: 1, + }, + expected: true, + }, + { + name: "LTE: height < other", + op: "LTE", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + expected: true, + }, + { + name: "LTE: height == other", + op: "LTE", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: true, + }, + { + name: "LTE: height > other", + op: "LTE", + height: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: false, + }, + { + name: "GT: height < other", + op: "GT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + expected: false, + }, + { + name: "GT: height == other", + op: "GT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: false, + }, + { + name: "GT: height > other", + op: "GT", + height: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: true, + }, + { + name: "GT: height < other (same revision number)", + op: "GT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 2, + }, + expected: false, + }, + { + name: "GT: height > other (same revision number)", + op: "GT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 2, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: true, + }, + { + name: "GT: height > other (same revision height)", + op: "GT", + height: &Height{ + RevisionNumber: 2, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: true, + }, + { + name: "GT: height < other (same revision height)", + op: "GT", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 2, + RevisionHeight: 1, + }, + expected: false, + }, + { + name: "GTE: height < other", + op: "GTE", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + expected: false, + }, + { + name: "GTE: height == other", + op: "GTE", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: true, + }, + { + name: "GTE: height > other", + op: "GTE", + height: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: true, + }, + { + name: "EQ: height < other", + op: "EQ", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + expected: false, + }, + { + name: "EQ: height == other", + op: "EQ", + height: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: true, + }, + { + name: "EQ: height > other", + op: "EQ", + height: &Height{ + RevisionNumber: 2, + RevisionHeight: 2, + }, + other: &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + switch tc.op { + case "LT": + require.Equal(t, tc.expected, tc.height.LT(tc.other)) + case "LTE": + require.Equal(t, tc.expected, tc.height.LTE(tc.other)) + case "GT": + require.Equal(t, tc.expected, tc.height.GT(tc.other)) + case "GTE": + require.Equal(t, tc.expected, tc.height.GTE(tc.other)) + case "EQ": + require.Equal(t, tc.expected, tc.height.EQ(tc.other)) + default: + panic(fmt.Sprintf("invalid comparison op: %s", tc.op)) + } + }) + } +} diff --git a/ibc/client/types/misbehaviour.go b/ibc/client/types/misbehaviour.go new file mode 100644 index 000000000..823ab6f28 --- /dev/null +++ b/ibc/client/types/misbehaviour.go @@ -0,0 +1,22 @@ +package types + +import ( + "errors" + + "github.com/pokt-network/pocket/shared/modules" +) + +var _ modules.ClientMessage = (*Misbehaviour)(nil) + +// ClientType is Wasm light client +func (m *Misbehaviour) ClientType() string { + return WasmClientType +} + +// ValidateBasic implements Misbehaviour interface +func (m *Misbehaviour) ValidateBasic() error { + if len(m.Data) == 0 { + return errors.New("data cannot be empty") + } + return nil +} diff --git a/ibc/client/types/queries.go b/ibc/client/types/queries.go new file mode 100644 index 000000000..fd7a8d8be --- /dev/null +++ b/ibc/client/types/queries.go @@ -0,0 +1,63 @@ +package types + +import ( + "github.com/pokt-network/pocket/ibc/path" + "github.com/pokt-network/pocket/shared/codec" + "github.com/pokt-network/pocket/shared/modules" +) + +// GetConsensusState returns the consensus state at the given height from a +// prefixed client store, in the format: "clients/{clientID}" +func GetConsensusState(clientStore modules.ProvableStore, height modules.Height) (modules.ConsensusState, error) { + // Retrieve the consensus state bytes from the client store + consStateBz, err := clientStore.Get(path.ConsensusStateKey(height.ToString())) + if err != nil { + return nil, err + } + + // Unmarshal into a ConsensusState interface + consState := new(ConsensusState) + if err := codec.GetCodec().Unmarshal(consStateBz, consState); err != nil { + return nil, err + } + + return consState, nil +} + +// GetClientState returns the client state from a prefixed client store, +// in the format: "clients" using the clientID provided +func GetClientState(clientStore modules.ProvableStore, identifier string) (modules.ClientState, error) { + // Retrieve the client state bytes from the client store + clientStateBz, err := clientStore.Get(path.ClientStateKey(identifier)) + if err != nil { + return nil, err + } + + // Unmarshal into a ClientState interface + clientState := new(ClientState) + if err := codec.GetCodec().Unmarshal(clientStateBz, clientState); err != nil { + return nil, err + } + + return clientState, nil +} + +// setClientState stores the client state +// clientStore must be a prefixed client store: "clients/{clientID}" +func setClientState(clientStore modules.ProvableStore, clientState *ClientState) error { + val, err := codec.GetCodec().Marshal(clientState) + if err != nil { + return err + } + return clientStore.Set([]byte(path.KeyClientState), val) // key == nil ==> key == "clients/{clientID}" +} + +// setConsensusState stores the consensus state at the given height. +// clientStore must be a prefixed client store: "clients/{clientID}" +func setConsensusState(clientStore modules.ProvableStore, consensusState *ConsensusState, height modules.Height) error { + val, err := codec.GetCodec().Marshal(consensusState) + if err != nil { + return err + } + return clientStore.Set(path.ConsensusStateKey(height.ToString()), val) +} diff --git a/ibc/client/types/queries_test.go b/ibc/client/types/queries_test.go new file mode 100644 index 000000000..cbca75d54 --- /dev/null +++ b/ibc/client/types/queries_test.go @@ -0,0 +1,411 @@ +package types + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "testing" + + "github.com/golang/mock/gomock" + "github.com/pokt-network/pocket/ibc/store" + "github.com/pokt-network/pocket/persistence/kvstore" + "github.com/pokt-network/pocket/runtime" + "github.com/pokt-network/pocket/runtime/configs" + "github.com/pokt-network/pocket/runtime/test_artifacts" + "github.com/pokt-network/pocket/shared/codec" + core_types "github.com/pokt-network/pocket/shared/core/types" + "github.com/pokt-network/pocket/shared/modules" + mock_modules "github.com/pokt-network/pocket/shared/modules/mocks" + "github.com/pokt-network/smt" + "github.com/stretchr/testify/require" +) + +func TestClientState_Set(t *testing.T) { + // get provable store prefixed with clients/123 + provableStore := newTestProvableStore(t, "123") + + // create a client state + clientState := &ClientState{ + Data: []byte("data"), + WasmChecksum: make([]byte, 32), + } + bz, err := codec.GetCodec().Marshal(clientState) + require.NoError(t, err) + + // set the client state + require.NoError(t, setClientState(provableStore, clientState)) + + // check cache + cache := kvstore.NewMemKVStore() + + // flush cache + require.NoError(t, provableStore.FlushCache(cache)) + + // get all from cache + keys, vals, err := cache.GetAll([]byte{}, false) + require.NoError(t, err) + require.Len(t, keys, 1) + require.Len(t, vals, 1) + + // check key and value set correctly + require.Equal(t, []byte("clients/123/1/clients/123/clientState"), keys[0]) + require.Equal(t, vals[0], bz) +} + +func TestConsensusState_Set(t *testing.T) { + // get provable store prefixed with clients/123 + provableStore := newTestProvableStore(t, "123") + + // create a consensus state + consensusState := &ConsensusState{ + Data: []byte("data"), + Timestamp: 1, + } + height := &Height{ + RevisionNumber: 1, + RevisionHeight: 1, + } + bz, err := codec.GetCodec().Marshal(consensusState) + require.NoError(t, err) + + // set the client state + require.NoError(t, setConsensusState(provableStore, consensusState, height)) + + // check cache + cache := kvstore.NewMemKVStore() + + // flush cache + require.NoError(t, provableStore.FlushCache(cache)) + + // get all from cache + keys, vals, err := cache.GetAll([]byte{}, false) + require.NoError(t, err) + require.Len(t, keys, 1) + require.Len(t, vals, 1) + + // check key and value set correctly + require.Equal(t, []byte("clients/123/1/clients/123/consensusStates/1-1"), keys[0]) + require.Equal(t, vals[0], bz) +} + +func TestClientState_Get(t *testing.T) { + clientStore := newTestProvableStore(t, "") + + testCases := []struct { + name string + clientId string + data []byte + checksum []byte + expectedErr error + }{ + { + name: "client state not found", + clientId: "124", + data: nil, + checksum: nil, + expectedErr: core_types.ErrIBCKeyDoesNotExist("clients/124/clientState"), + }, + { + name: "client state found", + clientId: "123", + data: []byte("data"), + checksum: make([]byte, 32), + expectedErr: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + clientState, err := GetClientState(clientStore, tc.clientId) + require.ErrorIs(t, err, tc.expectedErr) + if tc.expectedErr == nil { + require.Equal(t, clientState.GetData(), tc.data) + require.Equal(t, clientState.GetWasmChecksum(), tc.checksum) + } + }) + } +} + +func TestConsensusState_Get(t *testing.T) { + clientStore := newTestProvableStore(t, "123") + + testCases := []struct { + name string + height *Height + data []byte + timestamp uint64 + expectedErr error + }{ + { + name: "consensus state not found - wrong height", + height: &Height{RevisionNumber: 1, RevisionHeight: 2}, + data: nil, + timestamp: 0, + expectedErr: core_types.ErrIBCKeyDoesNotExist("clients/123/consensusStates/1-2"), + }, + { + name: "consensus state found", + height: &Height{RevisionNumber: 1, RevisionHeight: 1}, + data: []byte("data"), + timestamp: 1, + expectedErr: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + consensusState, err := GetConsensusState(clientStore, tc.height) + require.ErrorIs(t, err, tc.expectedErr) + if tc.expectedErr == nil { + require.Equal(t, consensusState.GetData(), tc.data) + require.Equal(t, consensusState.GetTimestamp(), tc.timestamp) + } + }) + } +} + +func newTestProvableStore(t *testing.T, clientId string) modules.ProvableStore { + t.Helper() + + tree, nodeStore, dbMap := setupDB(t) + + runtimeCfg := newTestRuntimeConfig(t) + bus, err := runtime.CreateBus(runtimeCfg) + require.NoError(t, err) + + persistenceMock := newPersistenceMock(t, bus, dbMap) + bus.RegisterModule(persistenceMock) + consensusMock := newConsensusMock(t, bus) + bus.RegisterModule(consensusMock) + treeStoreMock := newTreeStoreMock(t, bus, tree, nodeStore) + bus.RegisterModule(treeStoreMock) + p2pMock := newTestP2PModule(t, bus) + bus.RegisterModule(p2pMock) + utilityMock := newUtilityMock(t, bus) + bus.RegisterModule(utilityMock) + + privKey := runtimeCfg.GetConfig().IBC.Host.PrivateKey + + t.Cleanup(func() { + err := persistenceMock.Stop() + require.NoError(t, err) + err = consensusMock.Stop() + require.NoError(t, err) + err = p2pMock.Stop() + require.NoError(t, err) + }) + + if clientId != "" { + clientId = "/" + clientId + } + + return store.NewProvableStore(bus, []byte("clients"+clientId), privKey) +} + +func setupDB(t *testing.T) (*smt.SMT, kvstore.KVStore, map[string]string) { + dbMap := make(map[string]string, 0) + nodeStore := kvstore.NewMemKVStore() + tree := smt.NewSparseMerkleTree(nodeStore, sha256.New()) + + clientState := &ClientState{ + Data: []byte("data"), + WasmChecksum: make([]byte, 32), + } + cliBz, err := codec.GetCodec().Marshal(clientState) + require.NoError(t, err) + consensusState := &ConsensusState{ + Data: []byte("data"), + Timestamp: 1, + } + conBz, err := codec.GetCodec().Marshal(consensusState) + require.NoError(t, err) + + keys := [][]byte{ + []byte("clients/123/consensusStates/1-1"), + []byte("clients/123/clientState"), + } + values := [][]byte{ + conBz, + cliBz, + } + + for i, key := range keys { + dbMap[hex.EncodeToString(key)] = hex.EncodeToString(values[i]) + err := tree.Update(key, values[i]) + require.NoError(t, err) + } + + require.NoError(t, tree.Commit()) + + t.Cleanup(func() { + err := nodeStore.Stop() + require.NoError(t, err) + }) + + return tree, nodeStore, dbMap +} + +func newConsensusMock(t *testing.T, bus modules.Bus) *mock_modules.MockConsensusModule { + t.Helper() + + ctrl := gomock.NewController(t) + consensusMock := mock_modules.NewMockConsensusModule(ctrl) + consensusMock.EXPECT().GetModuleName().Return(modules.ConsensusModuleName).AnyTimes() + consensusMock.EXPECT().Start().Return(nil).AnyTimes() + consensusMock.EXPECT().Stop().Return(nil).AnyTimes() + consensusMock.EXPECT().SetBus(gomock.Any()).Return().AnyTimes() + consensusMock.EXPECT().GetBus().Return(bus).AnyTimes() + consensusMock.EXPECT().CurrentHeight().Return(uint64(1)).AnyTimes() + + return consensusMock +} + +func newUtilityMock(t *testing.T, bus modules.Bus) *mock_modules.MockUtilityModule { + t.Helper() + + ctrl := gomock.NewController(t) + utilityMock := mock_modules.NewMockUtilityModule(ctrl) + utilityMock.EXPECT().GetModuleName().Return(modules.UtilityModuleName).AnyTimes() + utilityMock.EXPECT().Start().Return(nil).AnyTimes() + utilityMock.EXPECT().Stop().Return(nil).AnyTimes() + utilityMock.EXPECT().SetBus(bus).Return().AnyTimes() + utilityMock.EXPECT().GetBus().Return(bus).AnyTimes() + utilityMock.EXPECT().HandleTransaction(gomock.Any()).Return(nil).AnyTimes() + + return utilityMock +} + +func newPersistenceMock(t *testing.T, + bus modules.Bus, + dbMap map[string]string, +) *mock_modules.MockPersistenceModule { + t.Helper() + + ctrl := gomock.NewController(t) + persistenceMock := mock_modules.NewMockPersistenceModule(ctrl) + persistenceReadContextMock := mock_modules.NewMockPersistenceReadContext(ctrl) + + persistenceMock.EXPECT().GetModuleName().Return(modules.PersistenceModuleName).AnyTimes() + persistenceMock.EXPECT().Start().Return(nil).AnyTimes() + persistenceMock.EXPECT().Stop().Return(nil).AnyTimes() + persistenceMock.EXPECT().SetBus(gomock.Any()).Return().AnyTimes() + persistenceMock.EXPECT().GetBus().Return(bus).AnyTimes() + persistenceMock.EXPECT().NewReadContext(gomock.Any()).Return(persistenceReadContextMock, nil).AnyTimes() + + persistenceMock.EXPECT().ReleaseWriteContext().Return(nil).AnyTimes() + persistenceReadContextMock. + EXPECT(). + GetIBCStoreEntry(gomock.Any(), gomock.Any()). + DoAndReturn( + func(key []byte, _ uint64) ([]byte, error) { + value, ok := dbMap[hex.EncodeToString(key)] + if !ok { + return nil, core_types.ErrIBCKeyDoesNotExist(string(key)) + } + bz, err := hex.DecodeString(value) + if err != nil { + return nil, err + } + if bytes.Equal(bz, nil) { + return nil, core_types.ErrIBCKeyDoesNotExist(string(key)) + } + return bz, nil + }). + AnyTimes() + + persistenceReadContextMock. + EXPECT(). + Release(). + AnyTimes() + + return persistenceMock +} + +func newTreeStoreMock(t *testing.T, + bus modules.Bus, + tree *smt.SMT, + nodeStore kvstore.KVStore, +) *mock_modules.MockTreeStoreModule { + t.Helper() + + ctrl := gomock.NewController(t) + treeStoreMock := mock_modules.NewMockTreeStoreModule(ctrl) + treeStoreMock.EXPECT().GetModuleName().Return(modules.TreeStoreSubmoduleName).AnyTimes() + treeStoreMock.EXPECT().SetBus(gomock.Any()).Return().AnyTimes() + treeStoreMock.EXPECT().GetBus().Return(bus).AnyTimes() + + treeStoreMock. + EXPECT(). + GetTree(gomock.Any()). + DoAndReturn( + func(_ string) ([]byte, kvstore.KVStore) { + return tree.Root(), nodeStore + }). + AnyTimes() + + return treeStoreMock +} + +func newTestP2PModule(t *testing.T, bus modules.Bus) modules.P2PModule { + t.Helper() + + ctrl := gomock.NewController(t) + p2pMock := mock_modules.NewMockP2PModule(ctrl) + + p2pMock.EXPECT().Start().Return(nil).AnyTimes() + p2pMock.EXPECT().Stop().Return(nil).AnyTimes() + p2pMock.EXPECT().SetBus(gomock.Any()).Return().AnyTimes() + p2pMock.EXPECT().GetBus().Return(bus).AnyTimes() + p2pMock.EXPECT(). + Broadcast(gomock.Any()). + Return(nil). + AnyTimes() + p2pMock.EXPECT(). + Send(gomock.Any(), gomock.Any()). + Return(nil). + AnyTimes() + p2pMock.EXPECT().GetModuleName().Return(modules.P2PModuleName).AnyTimes() + p2pMock.EXPECT().HandleEvent(gomock.Any()).Return(nil).AnyTimes() + + return p2pMock +} + +// TECHDEBT: centralise these helper functions in internal/testutils +func newTestRuntimeConfig(t *testing.T) *runtime.Manager { + t.Helper() + cfg, err := configs.CreateTempConfig(&configs.Config{ + Consensus: &configs.ConsensusConfig{ + PrivateKey: "0ca1a40ddecdab4f5b04fa0bfed1d235beaa2b8082e7554425607516f0862075dfe357de55649e6d2ce889acf15eb77e94ab3c5756fe46d3c7538d37f27f115e", + }, + Utility: &configs.UtilityConfig{ + MaxMempoolTransactionBytes: 1000000, + MaxMempoolTransactions: 1000, + }, + Persistence: &configs.PersistenceConfig{ + PostgresUrl: "", + NodeSchema: "test_schema", + BlockStorePath: ":memory:", + TxIndexerPath: ":memory:", + TreesStoreDir: ":memory:", + MaxConnsCount: 50, + MinConnsCount: 1, + MaxConnLifetime: "5m", + MaxConnIdleTime: "1m", + HealthCheckPeriod: "30s", + }, + Validator: &configs.ValidatorConfig{Enabled: true}, + IBC: &configs.IBCConfig{ + Enabled: true, + StoresDir: ":memory:", + Host: &configs.IBCHostConfig{ + PrivateKey: "0ca1a40ddecdab4f5b04fa0bfed1d235beaa2b8082e7554425607516f0862075dfe357de55649e6d2ce889acf15eb77e94ab3c5756fe46d3c7538d37f27f115e", + }, + }, + }) + if err != nil { + t.Fatalf("Error creating config: %s", err) + } + genesisState, _ := test_artifacts.NewGenesisState(0, 0, 0, 0) + runtimeCfg := runtime.NewManager(cfg, genesisState) + return runtimeCfg +} diff --git a/ibc/client/types/update.go b/ibc/client/types/update.go new file mode 100644 index 000000000..1fefaea0a --- /dev/null +++ b/ibc/client/types/update.go @@ -0,0 +1,123 @@ +package types + +import ( + "github.com/pokt-network/pocket/shared/modules" +) + +//nolint:unused // types defined for future use +type ( + clientMessage struct { + Header *Header `json:"header,omitempty"` + Misbehaviour *Misbehaviour `json:"misbehaviour,omitempty"` + } + verifyClientMessageInnerPayload struct { + ClientMessage clientMessage `json:"client_message"` + } + verifyClientMessagePayload struct { + VerifyClientMessage verifyClientMessageInnerPayload `json:"verify_client_message"` + } +) + +// 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 +func (cs *ClientState) VerifyClientMessage(clientStore modules.ProvableStore, clientMsg modules.ClientMessage) error { + clientMsgConcrete := clientMessage{ + Header: nil, + Misbehaviour: nil, + } + switch clientMsg := clientMsg.(type) { + case *Header: + clientMsgConcrete.Header = clientMsg + case *Misbehaviour: + clientMsgConcrete.Misbehaviour = clientMsg + } + + /* + inner := verifyClientMessageInnerPayload{ + ClientMessage: clientMsgConcrete, + } + payload := verifyClientMessagePayload{ + VerifyClientMessage: inner, + } + + // TODO(#912): implement WASM method calls + */ + + return nil +} + +//nolint:unused // types defined for future use +type ( + updateStateInnerPayload struct { + ClientMessage clientMessage `json:"client_message"` + } + updateStatePayload struct { + UpdateState updateStateInnerPayload `json:"update_state"` + } +) + +// UpdateState updates and stores as necessary any associated information for an +// IBC client. Upon successful update, a consensus height is returned. +// +// Client state and new consensus states are updated in the store by the contract +// Assumes the ClientMessage has already been verified +func (cs *ClientState) UpdateState(clientStore modules.ProvableStore, clientMsg modules.ClientMessage) (modules.Height, error) { + /* + header, ok := clientMsg.(*Header) + if !ok { + return nil, errors.New("client message must be a header") + } + + payload := updateStatePayload{ + UpdateState: updateStateInnerPayload{ + ClientMessage: clientMessage{ + Header: header, + }, + }, + } + + // TODO(#912): implement WASM method calls + */ + + return clientMsg.(*Header).Height, nil +} + +//nolint:unused // types defined for future use +type ( + updateStateOnMisbehaviourInnerPayload struct { + ClientMessage clientMessage `json:"client_message"` + } + updateStateOnMisbehaviourPayload struct { + UpdateStateOnMisbehaviour updateStateOnMisbehaviourInnerPayload `json:"update_state_on_misbehaviour"` + } +) + +// 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(clientStore modules.ProvableStore, clientMsg modules.ClientMessage) error { + var clientMsgConcrete clientMessage + switch clientMsg := clientMsg.(type) { + case *Header: + clientMsgConcrete.Header = clientMsg + case *Misbehaviour: + clientMsgConcrete.Misbehaviour = clientMsg + } + + /* + inner := updateStateOnMisbehaviourInnerPayload{ + ClientMessage: clientMsgConcrete, + } + + payload := updateStateOnMisbehaviourPayload{ + UpdateStateOnMisbehaviour: inner, + } + + // TODO(#912): implement WASM method calls + */ + + return nil +} diff --git a/ibc/client/types/upgrade.go b/ibc/client/types/upgrade.go new file mode 100644 index 000000000..9f89e06da --- /dev/null +++ b/ibc/client/types/upgrade.go @@ -0,0 +1,73 @@ +package types + +import ( + "fmt" + + "github.com/pokt-network/pocket/shared/modules" +) + +//nolint:unused // types defined for future use +type ( + verifyUpgradeAndUpdateStateInnerPayload struct { + UpgradeClientState modules.ClientState `json:"upgrade_client_state"` + UpgradeConsensusState modules.ConsensusState `json:"upgrade_consensus_state"` + ProofUpgradeClient []byte `json:"proof_upgrade_client"` + ProofUpgradeConsensusState []byte `json:"proof_upgrade_consensus_state"` + } + verifyUpgradeAndUpdateStatePayload struct { + VerifyUpgradeAndUpdateState verifyUpgradeAndUpdateStateInnerPayload `json:"verify_upgrade_and_update_state"` + } +) + +// 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( + clientStore modules.ProvableStore, + upgradedClient modules.ClientState, + upgradedConsState modules.ConsensusState, + proofUpgradeClient, proofUpgradeConsState []byte, +) error { + //nolint:gocritic // Commented out code is for future us + /* + wasmUpgradeClientState, ok := upgradedClient.(*ClientState) + if !ok { + return errors.New("upgraded client state must be Wasm ClientState") + } + + wasmUpgradeConsState, ok := upgradedConsState.(*ConsensusState) + if !ok { + return errors.New("upgraded consensus state must be Wasm ConsensusState") + } + */ + + // last height of current counterparty chain must be client's latest height + lastHeight := cs.GetLatestHeight() + + if !upgradedClient.GetLatestHeight().GT(lastHeight) { + return fmt.Errorf("upgraded client height %s must be greater than current client height %s", + upgradedClient.GetLatestHeight(), lastHeight, + ) + } + + // Must prove against latest consensus state to ensure we are verifying + // against latest upgrade plan. + _, err := GetConsensusState(clientStore, lastHeight) + if err != nil { + return fmt.Errorf("could not retrieve consensus state for height %s", lastHeight) + } + + /* + payload := verifyUpgradeAndUpdateStatePayload{ + VerifyUpgradeAndUpdateState: verifyUpgradeAndUpdateStateInnerPayload{ + UpgradeClientState: upgradedClient, + UpgradeConsensusState: upgradedConsState, + ProofUpgradeClient: proofUpgradeClient, + ProofUpgradeConsensusState: proofUpgradeConsState, + }, + } + + // TODO(#912): implement WASM contract initialisation + */ + + return nil +} diff --git a/ibc/client/types/validate_test.go b/ibc/client/types/validate_test.go new file mode 100644 index 000000000..80a1fa4bf --- /dev/null +++ b/ibc/client/types/validate_test.go @@ -0,0 +1,172 @@ +package types + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestClientState_Validate(t *testing.T) { + testCases := []struct { + name string + clientState *ClientState + expectedErr error + }{ + { + name: "valid client state", + clientState: &ClientState{ + Data: []byte("data"), + WasmChecksum: make([]byte, 32), + }, + expectedErr: nil, + }, + { + name: "invalid client state: empty data", + clientState: &ClientState{ + Data: nil, + WasmChecksum: make([]byte, 32), + }, + expectedErr: errors.New("data cannot be empty"), + }, + { + name: "invalid client state: empty wasm checksum", + clientState: &ClientState{ + Data: []byte("data"), + WasmChecksum: nil, + }, + expectedErr: errors.New("wasm checksum cannot be empty"), + }, + { + name: "invalid client state: invalid wasm checksum", + clientState: &ClientState{ + Data: []byte("data"), + WasmChecksum: []byte("invalid"), + }, + expectedErr: errors.New("expected 32, got 7"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.clientState.Validate() + if tc.expectedErr != nil { + require.ErrorAs(t, err, &tc.expectedErr) + return + } + require.NoError(t, err) + }) + } +} + +func TestConsensusState_ValidateBasic(t *testing.T) { + testCases := []struct { + name string + consensusState *ConsensusState + expectedErr error + }{ + { + name: "valid consensus state", + consensusState: &ConsensusState{ + Timestamp: 1, + Data: []byte("data"), + }, + expectedErr: nil, + }, + { + name: "invalid consensus state: zero timestamp", + consensusState: &ConsensusState{ + Timestamp: 0, + Data: []byte("data"), + }, + expectedErr: errors.New("timestamp must be a positive Unix time"), + }, + { + name: "invalid consensus state: empty data", + consensusState: &ConsensusState{ + Timestamp: 1, + Data: nil, + }, + expectedErr: errors.New("data cannot be empty"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.consensusState.ValidateBasic() + if tc.expectedErr != nil { + require.ErrorAs(t, err, &tc.expectedErr) + return + } + require.NoError(t, err) + }) + } +} + +func TestHeader_ValidateBasic(t *testing.T) { + testCases := []struct { + name string + header *Header + expectedErr error + }{ + { + name: "valid header", + header: &Header{ + Data: []byte("data"), + }, + expectedErr: nil, + }, + { + name: "invalid header: empty data", + header: &Header{ + Data: nil, + }, + expectedErr: errors.New("data cannot be empty"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.header.ValidateBasic() + if tc.expectedErr != nil { + require.ErrorAs(t, err, &tc.expectedErr) + return + } + require.NoError(t, err) + }) + } +} + +func TestMisbehaviour_ValidateBasic(t *testing.T) { + testCases := []struct { + name string + misbehaviour *Misbehaviour + expectedErr error + }{ + { + name: "valid misbehaviour", + misbehaviour: &Misbehaviour{ + Data: []byte("data"), + }, + expectedErr: nil, + }, + { + name: "invalid misbehaviour: empty data", + misbehaviour: &Misbehaviour{ + Data: nil, + }, + expectedErr: errors.New("data cannot be empty"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.misbehaviour.ValidateBasic() + if tc.expectedErr != nil { + require.ErrorAs(t, err, &tc.expectedErr) + return + } + require.NoError(t, err) + }) + } +} diff --git a/ibc/docs/README.md b/ibc/docs/README.md index 28160a08b..be84e362e 100644 --- a/ibc/docs/README.md +++ b/ibc/docs/README.md @@ -10,6 +10,7 @@ - [Components](#components) - [ICS-24 Host Requirements](#ics-24-host-requirements) - [ICS-23 Vector Commitments](#ics-23-vector-commitments) + - [ICS-02 Client Semantics](#ics-02-client-semantics) ## Definitions @@ -115,7 +116,15 @@ See: [ICS-24](./ics24.md) for more details on the specifics of the ICS-24 implem See: [ICS-23](./ics23.md) for more details on the specifics of the ICS-23 implementation for Pocket. +### ICS-02 Client Semantics + +[ICS-02][ics02] defines the methods, and interfaces through which the IBC host will interact with and manage the different clients it uses. This includes the creation of clients, their updates and upgrades as well as verifying any proofs with the counterparty client's state. The following interfaces must be defined: `ClientState`, `ConsensusState`, `ClientMessage`, `Height` each of these will potentially have a different implementation for each client type. In order to improve client upgradeability Pocket uses [ICS-08][ics08] WASM clients, which use a generic implementation of each interface, passing in opaque serialised data to the WASM client to be deserialised and used internally. + +See [ICS-02](./ics02.md) for more details on the specifics of the ICS-02 implementation for Pocket. + [ibc-spec]: https://github.com/cosmos/ibc [ics24]: https://github.com/cosmos/ibc/blob/main/spec/core/ics-024-host-requirements/README.md [ics23]: https://github.com/cosmos/ibc/blob/main/spec/core/ics-023-vector-commitments/README.md [smt]: https://github.com/pokt-network/smt +[ics02]: https://github.com/cosmos/ibc/blob/main/spec/core/ics-002-client-semantics/README.md +[ics08]: https://github.com/cosmos/ibc/blob/main/spec/client/ics-008-wasm-client/README.md diff --git a/ibc/docs/ics02.md b/ibc/docs/ics02.md new file mode 100644 index 000000000..dfa64d2a7 --- /dev/null +++ b/ibc/docs/ics02.md @@ -0,0 +1,156 @@ +# ICS-02 Client Semantics + +- [Definitions](#definitions) + - ["light client"](#light-client) +- [Overview](#overview) +- [Implementation](#implementation) + - [Client Manager](#client-manager) + - [Lifecycle Management](#lifecycle-management) + - [Client Queries](#client-queries) +- [Types](#types) +- [Provable Stores](#provable-stores) + +## Definitions + +### "light client" + +In the context of IBC a light client differs from a traditional "light client." An IBC light client is simply a state verification algorithm. It does not sync with the network, it does not download headers. Instead the updates/new headers for a client are provided by an IBC relayer. + +## Overview + +IBC utilises light clients to verify the correctness of the state of a counterparty chain. This allows for an IBC packet to be committed to in the state of the network on a source chain and then validated through the light client on the counterparty chain. + +[ICS-02][ics02] defines the interfaces and types through which the host machine can interact with the light clients it manages. This includes: client creation, client updates and upgrades as well as submitting misbehaviour from the chain the client is tracking. In addition to this, ICS-02 also defines numerous interfaces that are used by the different client implementations in order to carry out the previous actions as well as verify the state of the chain they represent via a proof. + +## Implementation + +[ICS-02][ics02] is implemented according to the specification. However as the Pocket protocol will utilise [ICS-08][ics08] WASM clients for the improvements to client upgradeability; the implementations of the `ClientState`, `ConsensusState` and other interfaces are specific to a WASM client. + +The implementation details are explored below, the code for ICS-02 can be found in [ibc/client](../client/) + +### Client Manager + +The `ClientManager` is the submodule that governs the light client implementations and implements the [ICS-02][ics02] interface. It is defined in [shared/modules/ibc_client_module.go](../../shared/modules/ibc_client_module.go). The `ClientManager` exposed the following methods: + +```go +// === Client Lifecycle Management === + +// CreateClient creates a new client with the given client state and initial consensus state +// and initialises its unique identifier in the IBC store +CreateClient(ClientState, ConsensusState) (string, error) + +// UpdateClient updates an existing client with the given ClientMessage, given that +// the ClientMessage can be verified using the existing ClientState and ConsensusState +UpdateClient(identifier string, clientMessage ClientMessage) error + +// UpgradeClient upgrades an existing client with the given identifier using the +// ClientState and ConsenusState provided. It can only do so if the new client +// was committed to by the old client at the specified upgrade height +UpgradeClient( + identifier string, + clientState ClientState, consensusState ConsensusState, + proofUpgradeClient, proofUpgradeConsState []byte, +) error + +// === Client Queries === + +// GetConsensusState returns the ConsensusState at the given height for the given client +GetConsensusState(identifier string, height Height) (ConsensusState, error) + +// GetClientState returns the ClientState for the given client +GetClientState(identifier string) (ClientState, error) + +// GetHostConsensusState returns the ConsensusState at the given height for the host chain +GetHostConsensusState(height Height) (ConsensusState, error) + +// GetHostClientState returns the ClientState at the provided height for the host chain +GetHostClientState(height Height) (ClientState, error) + +// GetCurrentHeight returns the current IBC client height of the network +GetCurrentHeight() Height + +// VerifyHostClientState verifies the client state for a client running on a +// counterparty chain is valid, checking against the current host client state +VerifyHostClientState(ClientState) error +``` + +#### Lifecycle Management + +The `ClientManager` handles the creation, updates and upgrades for a light client. It does so by utilising the following interfaces: + +```go +type ClientState interface +type ConsensusState interface +type ClientMessage interface +``` + +These interfaces are generic but have unique implementations for each client type. As Pocket utilises WASM light clients each implementation contains a `data []byte` field which contains a serialised, opaque data structure for use within the WASM client. + +The `data` field is a JSON serialised payload that contains the data required for the client to carry out the desired operation, as well as the operation name to carry out. For example, a verify membership payload is constructed using the following `struct`s: + +```go +type ( + verifyMembershipInnerPayload struct { + Height modules.Height `json:"height"` + DelayTimePeriod uint64 `json:"delay_time_period"` + DelayBlockPeriod uint64 `json:"delay_block_period"` + Proof []byte `json:"proof"` + Path core_types.CommitmentPath `json:"path"` + Value []byte `json:"value"` + } + verifyMembershipPayload struct { + VerifyMembership verifyMembershipInnerPayload `json:"verify_membership"` + } +) +``` + +By utilising this pattern of JSON payloads the WASM client itself is able to unmarshal the opaque payload into their own internal protobuf definitions for the implementation of the `ClientState` for example. This allows them to have a much simpler implementation and focus solely on the logic around verification and utilising simple storage. + +See: [Types](#types) for more information on the interfaces and types used in the ICS-02 implementation + +#### Client Queries + +[ICS-24](./ics24.md) instructs that a host must allow for the introspection of both its own `ConsensusState` and `ClientState`. This is done through the `ClientManager`'s `GetHostConsensusState` and `GetHostClientState` methods. These are then used by relayers to: + +1. Provide light clients running on counterparty chains the `ConsensusState` and `ClientState` objects they need. +2. Verify the state of a light client running on a counterparty chain, against the host chain's current `ClientState` + +The other queries used by the `ClientManager` involve querying the [ICS-24](./ics24.md) stores to retrieve the `ClientState` and `ConsensusState` stored objects on a per-client basis. + +See [Provable Stores](#provable-stores) for more information on how the `ProvableStore`s are used in ICS-02. + +## Types + +The [ICS-02 specification][ics02] defines the need for numerous interfaces: + +1. `ClientState` + - `ClientState` is an opaque data structure defined by a client type. It may keep arbitrary internal state to track verified roots and past misbehaviours. +2. `ConsensusState` + - `ConsensusState` is an opaque data structure defined by a client type, used by the + validity predicate to verify new commits & state roots. Likely the structure will contain the last commit produced by the consensus process, including signatures and validator set metadata. +3. `ClientMessage` + - `ClientMessage` is an opaque data structure defined by a client type which provides information to update the client. `ClientMessage`s can be submitted to an associated client to add new `ConsensusState`(s) and/or update the `ClientState`. They likely contain a height, a proof, a commitment root, and possibly updates to the validity predicate. +4. `Height` + - `Height` is an interface that defines the methods required by a clients implementation of their own height object `Height`s usually have two components: revision number and revision height. + +As previously mentioned these interfaces have different implementations for each light client type. This is due to the different light clients representing different networks, consensus types and chains altogether. The implementation of these interfaces can be found in [ibc/client/types/proto/wasm.proto](../client/types/proto/wasm.proto). + +The `data` field in these messages represents the opaque data structure that is internal to the WASM client. This is a part of the JSON serialised payload that is passed into the WASM client, and is used to carry out any relevant operations. This enables the WASM client to define its own internal data structures that can unmarshal the JSON payload into its own internal protobuf definitions. + +See: [shared/modules/ibc_client_module.go](../../shared/modules/ibc_client_module.go) for the details on the interfaces and their methods. + +## Provable Stores + +ICS-02 requires a lot of data to be stored in the IBC stores (defined in [ICS-24](./ics24.md)). In order to do this the provable stores must be initialised on a per client ID basis. This means that any operation using the provable store does not require the use of the `clientID`. This is done as follows: + +```go +prefixed := path.ApplyPrefix(core_types.CommitmentPrefix(path.KeyClientStorePrefix), identifier) +clientStore, err := c.GetBus().GetIBCHost().GetProvableStore(string(prefixed)) +``` + +This allows the `clientStore` to be used by the WASM clients without them needing to keep track of their unique identifiers. + +See: [ibc/client/submodule.go](../client/submodule.go) for more details on how this is used. + +[ics02]: https://github.com/cosmos/ibc/blob/main/spec/core/ics-002-client-semantics/README.md +[ics08]: https://github.com/cosmos/ibc/blob/main/spec/client/ics-008-wasm-client/README.md diff --git a/ibc/host/submodule.go b/ibc/host/submodule.go index 655985b73..ad95e72a9 100644 --- a/ibc/host/submodule.go +++ b/ibc/host/submodule.go @@ -4,6 +4,7 @@ import ( "errors" "time" + "github.com/pokt-network/pocket/ibc/client" "github.com/pokt-network/pocket/ibc/events" "github.com/pokt-network/pocket/ibc/store" "github.com/pokt-network/pocket/runtime/configs" @@ -20,10 +21,6 @@ type ibcHost struct { cfg *configs.IBCHostConfig logger *modules.Logger storesDir string - - // only a single bulk store cacher and event logger are allowed - bsc modules.BulkStoreCacher - em modules.EventLogger } func Create(bus modules.Bus, config *configs.IBCHostConfig, options ...modules.IBCHostOption) (modules.IBCHostSubmodule, error) { @@ -59,7 +56,7 @@ func (*ibcHost) Create(bus modules.Bus, config *configs.IBCHostConfig, options . bus.RegisterModule(h) - bsc, err := store.Create(h.GetBus(), + _, err := store.Create(h.GetBus(), h.cfg.BulkStoreCacher, store.WithLogger(h.logger), store.WithStoresDir(h.storesDir), @@ -68,13 +65,16 @@ func (*ibcHost) Create(bus modules.Bus, config *configs.IBCHostConfig, options . if err != nil { return nil, err } - h.bsc = bsc - em, err := events.Create(h.GetBus(), events.WithLogger(h.logger)) + _, err = events.Create(h.GetBus(), events.WithLogger(h.logger)) + if err != nil { + return nil, err + } + + _, err = client.Create(h.GetBus(), client.WithLogger(h.logger)) if err != nil { return nil, err } - h.em = em return h, nil } diff --git a/ibc/host_introspection_test.go b/ibc/host_introspection_test.go new file mode 100644 index 000000000..35f0af6b5 --- /dev/null +++ b/ibc/host_introspection_test.go @@ -0,0 +1,271 @@ +package ibc + +import ( + "errors" + "testing" + "time" + + ics23 "github.com/cosmos/ics23/go" + light_client_types "github.com/pokt-network/pocket/ibc/client/light_clients/types" + client_types "github.com/pokt-network/pocket/ibc/client/types" + "github.com/pokt-network/pocket/ibc/types" + "github.com/pokt-network/pocket/shared/codec" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/durationpb" +) + +func TestHost_GetCurrentHeight(t *testing.T) { + _, _, _, _, ibcMod := prepareEnvironment(t, 1, 0, 0, 0) + cm := ibcMod.GetBus().GetClientManager() + + // get the current height + height, err := cm.GetCurrentHeight() + require.NoError(t, err) + require.Equal(t, uint64(1), height.GetRevisionNumber()) + require.Equal(t, uint64(0), height.GetRevisionHeight()) + + // increment the height + publishNewHeightEvent(t, ibcMod.GetBus(), 1) + + height, err = cm.GetCurrentHeight() + require.NoError(t, err) + require.Equal(t, uint64(1), height.GetRevisionNumber()) + require.Equal(t, uint64(1), height.GetRevisionHeight()) +} + +func TestHost_GetHostConsensusState(t *testing.T) { + _, _, _, _, ibcMod := prepareEnvironment(t, 1, 0, 0, 0) + cm := ibcMod.GetBus().GetClientManager() + + consState, err := cm.GetHostConsensusState(&client_types.Height{RevisionNumber: 1, RevisionHeight: 0}) + require.NoError(t, err) + + require.Equal(t, "08-wasm", consState.ClientType()) + require.NoError(t, consState.ValidateBasic()) + require.Less(t, consState.GetTimestamp(), uint64(time.Now().UnixNano())) + + pocketConState := new(light_client_types.PocketConsensusState) + err = codec.GetCodec().Unmarshal(consState.GetData(), pocketConState) + require.NoError(t, err) + + blockstore := ibcMod.GetBus().GetPersistenceModule().GetBlockStore() + block, err := blockstore.GetBlock(0) + require.NoError(t, err) + + require.Equal(t, block.BlockHeader.Timestamp, pocketConState.Timestamp) + require.Equal(t, block.BlockHeader.StateHash, pocketConState.StateHash) + require.Equal(t, block.BlockHeader.StateTreeHashes, pocketConState.StateTreeHashes) + require.Equal(t, block.BlockHeader.NextValSetHash, pocketConState.NextValSetHash) +} + +func TestHost_GetHostClientState(t *testing.T) { + _, _, _, _, ibcMod := prepareEnvironment(t, 1, 0, 0, 0) + cm := ibcMod.GetBus().GetClientManager() + + clientState, err := cm.GetHostClientState(&client_types.Height{RevisionNumber: 1, RevisionHeight: 0}) + require.NoError(t, err) + require.Equal(t, "08-wasm", clientState.ClientType()) + + pocketClientState := new(light_client_types.PocketClientState) + err = codec.GetCodec().Unmarshal(clientState.GetData(), pocketClientState) + require.NoError(t, err) + + blockstore := ibcMod.GetBus().GetPersistenceModule().GetBlockStore() + block, err := blockstore.GetBlock(0) + require.NoError(t, err) + + require.Equal(t, pocketClientState.NetworkId, block.BlockHeader.NetworkId) + require.Equal(t, pocketClientState.TrustLevel, &light_client_types.Fraction{Numerator: 2, Denominator: 3}) + require.Equal(t, pocketClientState.TrustingPeriod.AsDuration().Nanoseconds(), int64(1814400000000000)) + require.Equal(t, pocketClientState.UnbondingPeriod.AsDuration().Nanoseconds(), int64(1814400000000000)) + require.Equal(t, pocketClientState.MaxClockDrift.AsDuration().Nanoseconds(), int64(900000000000)) + require.Equal(t, pocketClientState.LatestHeight, &client_types.Height{RevisionNumber: 1, RevisionHeight: 0}) + require.True(t, pocketClientState.ProofSpec.ConvertToIcs23ProofSpec().SpecEquals(ics23.SmtSpec)) +} + +func TestHost_VerifyHostClientState(t *testing.T) { + _, _, _, persistenceMod, ibcMod := prepareEnvironment(t, 1, 0, 0, 0) + cm := ibcMod.GetBus().GetClientManager() + + approxTime := time.Minute * 15 + unbondingPeriod := time.Duration(1814400000000000) * approxTime + blockstore := ibcMod.GetBus().GetPersistenceModule().GetBlockStore() + block, err := blockstore.GetBlock(0) + require.NoError(t, err) + + publishNewHeightEvent(t, ibcMod.GetBus(), 1) + + rwCtx, err := persistenceMod.NewRWContext(1) + require.NoError(t, err) + defer rwCtx.Release() + err = rwCtx.Commit(nil, nil) + require.NoError(t, err) + + testCases := []struct { + name string + pcs *light_client_types.PocketClientState + expectedErr error + }{ + { + name: "invalid: frozen client", + pcs: &light_client_types.PocketClientState{ + NetworkId: block.BlockHeader.NetworkId, + TrustLevel: &light_client_types.Fraction{Numerator: 2, Denominator: 3}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod), + MaxClockDrift: durationpb.New(approxTime), + LatestHeight: &client_types.Height{ + RevisionNumber: 1, + RevisionHeight: 0, + }, + ProofSpec: types.SmtSpec, + FrozenHeight: 1, + }, + expectedErr: errors.New("counterparty client state is frozen"), + }, + { + name: "invalid: different network id", + pcs: &light_client_types.PocketClientState{ + NetworkId: "not correct", + TrustLevel: &light_client_types.Fraction{Numerator: 2, Denominator: 3}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod), + MaxClockDrift: durationpb.New(approxTime), + LatestHeight: &client_types.Height{ + RevisionNumber: 1, + RevisionHeight: 0, + }, + ProofSpec: types.SmtSpec, + }, + expectedErr: errors.New("counterparty client state has a different network id"), + }, + { + name: "invalid: different revision number", + pcs: &light_client_types.PocketClientState{ + NetworkId: block.BlockHeader.NetworkId, + TrustLevel: &light_client_types.Fraction{Numerator: 2, Denominator: 3}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod), + MaxClockDrift: durationpb.New(approxTime), + LatestHeight: &client_types.Height{ + RevisionNumber: 0, + RevisionHeight: 0, + }, + ProofSpec: types.SmtSpec, + }, + expectedErr: errors.New("counterparty client state has a different revision number"), + }, + { + name: "invalid: equal height", + pcs: &light_client_types.PocketClientState{ + NetworkId: block.BlockHeader.NetworkId, + TrustLevel: &light_client_types.Fraction{Numerator: 2, Denominator: 3}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod), + MaxClockDrift: durationpb.New(approxTime), + LatestHeight: &client_types.Height{ + RevisionNumber: 1, + RevisionHeight: 1, + }, + ProofSpec: types.SmtSpec, + }, + expectedErr: errors.New("counterparty client state has a height greater than or equal to the host client state"), + }, + { + name: "invalid: wrong trust level", + pcs: &light_client_types.PocketClientState{ + NetworkId: block.BlockHeader.NetworkId, + TrustLevel: &light_client_types.Fraction{Numerator: 1, Denominator: 4}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod), + MaxClockDrift: durationpb.New(approxTime), + LatestHeight: &client_types.Height{ + RevisionNumber: 1, + RevisionHeight: 0, + }, + ProofSpec: types.SmtSpec, + }, + expectedErr: errors.New("counterparty client state trust level is not in the accepted range"), + }, + { + name: "invalid: different proof spec", + pcs: &light_client_types.PocketClientState{ + NetworkId: block.BlockHeader.NetworkId, + TrustLevel: &light_client_types.Fraction{Numerator: 2, Denominator: 3}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod), + MaxClockDrift: durationpb.New(approxTime), + LatestHeight: &client_types.Height{ + RevisionNumber: 1, + RevisionHeight: 0, + }, + ProofSpec: types.ConvertFromIcs23ProofSpec(ics23.IavlSpec), + }, + expectedErr: errors.New("counterparty client state has different proof spec"), + }, + { + name: "invalid: different unbonding period", + pcs: &light_client_types.PocketClientState{ + NetworkId: block.BlockHeader.NetworkId, + TrustLevel: &light_client_types.Fraction{Numerator: 2, Denominator: 3}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod + 1), + MaxClockDrift: durationpb.New(approxTime), + LatestHeight: &client_types.Height{ + RevisionNumber: 1, + RevisionHeight: 0, + }, + ProofSpec: types.SmtSpec, + }, + expectedErr: errors.New("counterparty client state has different unbonding period"), + }, + { + name: "invalid: unbonding period less than trusting period", + pcs: &light_client_types.PocketClientState{ + NetworkId: block.BlockHeader.NetworkId, + TrustLevel: &light_client_types.Fraction{Numerator: 2, Denominator: 3}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod - 1), + MaxClockDrift: durationpb.New(approxTime), + LatestHeight: &client_types.Height{ + RevisionNumber: 1, + RevisionHeight: 0, + }, + ProofSpec: types.SmtSpec, + }, + expectedErr: errors.New("counterparty client state unbonding period is less than trusting period"), + }, + { + name: "valid client state", + pcs: &light_client_types.PocketClientState{ + NetworkId: block.BlockHeader.NetworkId, + TrustLevel: &light_client_types.Fraction{Numerator: 2, Denominator: 3}, + TrustingPeriod: durationpb.New(unbondingPeriod), + UnbondingPeriod: durationpb.New(unbondingPeriod), + MaxClockDrift: durationpb.New(approxTime), + LatestHeight: &client_types.Height{ + RevisionNumber: 1, + RevisionHeight: 0, + }, + ProofSpec: types.SmtSpec, + }, + expectedErr: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + bz, err := codec.GetCodec().Marshal(tc.pcs) + require.NoError(t, err) + clientState := &client_types.ClientState{ + Data: bz, + RecentHeight: &client_types.Height{ + RevisionNumber: 1, + RevisionHeight: 0, + }, + } + err = cm.VerifyHostClientState(clientState) + require.ErrorAs(t, err, &tc.expectedErr) + }) + } +} diff --git a/ibc/main_test.go b/ibc/main_test.go index 51fa0c63f..bf93e685f 100644 --- a/ibc/main_test.go +++ b/ibc/main_test.go @@ -39,7 +39,7 @@ func newTestConsensusModule(t *testing.T, bus modules.Bus) modules.ConsensusModu return consensusMod.(modules.ConsensusModule) } -func newTestP2PModule(t *testing.T, bus modules.Bus) modules.P2PModule { +func newTestP2PModule(t *testing.T) modules.P2PModule { t.Helper() ctrl := gomock.NewController(t) @@ -57,7 +57,6 @@ func newTestP2PModule(t *testing.T, bus modules.Bus) modules.P2PModule { AnyTimes() p2pMock.EXPECT().GetModuleName().Return(modules.P2PModuleName).AnyTimes() p2pMock.EXPECT().HandleEvent(gomock.Any()).Return(nil).AnyTimes() - bus.RegisterModule(p2pMock) return p2pMock } @@ -117,7 +116,7 @@ func prepareEnvironment( require.NoError(t, err) bus.RegisterModule(testConsensusMod) - testP2PMock := newTestP2PModule(t, bus) + testP2PMock := newTestP2PModule(t) err = testP2PMock.Start() require.NoError(t, err) bus.RegisterModule(testP2PMock) diff --git a/ibc/path/keys_ics02.go b/ibc/path/keys_ics02.go index 5cce681be..7f06fd089 100644 --- a/ibc/path/keys_ics02.go +++ b/ibc/path/keys_ics02.go @@ -1,6 +1,8 @@ package path -import "fmt" +import ( + "fmt" +) //////////////////////////////////////////////////////////////////////////////// // ICS02 @@ -14,31 +16,43 @@ func FullClientStateKey(clientID string) []byte { return fullClientKey(clientID, KeyClientState) } -// ClientStatePath takes a client identifier and returns a Path string where it can be accessed +// clientStatePath takes a client identifier and returns a Path string where it can be accessed // within the client store -func ClientStatePath(clientID string) string { +func clientStatePath(clientID string) string { return clientPath(clientID, KeyClientState) } +// ClientStateKey takes a client identifier and returns a key where it can be accessed +// within the client store +func ClientStateKey(clientID string) []byte { + return []byte(clientStatePath(clientID)) +} + // consensusStatePath returns the suffix store key for the consensus state at a // particular height stored in a client prefixed store. -func consensusStatePath(height uint64) string { - return fmt.Sprintf("%s/%d", KeyConsensusStatePrefix, height) +func consensusStatePath(height string) string { + return fmt.Sprintf("%s/%s", KeyConsensusStatePrefix, height) +} + +// ConsensusStateKey returns the store key for the consensus state of a particular client +// in a prefixed client store +func ConsensusStateKey(height string) []byte { + return []byte(consensusStatePath(height)) } // fullConsensusStatePath takes a client identifier and returns a Path under which to // store the consensus state of a client. -func fullConsensusStatePath(clientID string, height uint64) string { +func fullConsensusStatePath(clientID, height string) string { return fullClientPath(clientID, consensusStatePath(height)) } // FullConsensusStateKey returns the store key for the consensus state of a particular client. -func FullConsensusStateKey(clientID string, height uint64) []byte { +func FullConsensusStateKey(clientID, height string) []byte { return []byte(fullConsensusStatePath(clientID, height)) } // ConsensusStatePath takes a client identifier and height and returns the Path where the consensus // state can be accessed in the client store -func ConsensusStatePath(clientID string, height uint64) string { +func ConsensusStatePath(clientID, height string) string { return clientPath(clientID, consensusStatePath(height)) } diff --git a/ibc/store/bulk_store_cache.go b/ibc/store/bulk_store_cache.go index 0e71de3cf..52f23bb34 100644 --- a/ibc/store/bulk_store_cache.go +++ b/ibc/store/bulk_store_cache.go @@ -92,7 +92,7 @@ func (s *bulkStoreCache) AddStore(name string) error { if _, ok := s.ls.stores[name]; ok { return coreTypes.ErrIBCStoreAlreadyExists(name) } - store := newProvableStore(s.GetBus(), coreTypes.CommitmentPrefix(name), s.privateKey) + store := NewProvableStore(s.GetBus(), coreTypes.CommitmentPrefix(name), s.privateKey) s.ls.stores[store.name] = store return nil } diff --git a/ibc/store/provable_store.go b/ibc/store/provable_store.go index 0ab6bd6c7..6c388230c 100644 --- a/ibc/store/provable_store.go +++ b/ibc/store/provable_store.go @@ -51,8 +51,8 @@ type provableStore struct { privateKey string } -// newProvableStore returns a new instance of provableStore with the bus and prefix provided -func newProvableStore(bus modules.Bus, prefix coreTypes.CommitmentPrefix, privateKey string) *provableStore { +// NewProvableStore returns a new instance of provableStore with the bus and prefix provided +func NewProvableStore(bus modules.Bus, prefix coreTypes.CommitmentPrefix, privateKey string) *provableStore { return &provableStore{ m: sync.Mutex{}, bus: bus, @@ -233,5 +233,7 @@ func applyPrefix(prefix coreTypes.CommitmentPrefix, key []byte) coreTypes.Commit if len(prefix) > len(slashed) && bytes.Equal(prefix[:len(slashed)], slashed) { return key } - return path.ApplyPrefix(prefix, string(key)) + prefixed := path.ApplyPrefix(prefix, string(key)) + trimmed := strings.TrimSuffix(string(prefixed), "/") + return coreTypes.CommitmentPath(trimmed) } diff --git a/ibc/store/provable_store_test.go b/ibc/store/provable_store_test.go index a62160a62..0fabc60a4 100644 --- a/ibc/store/provable_store_test.go +++ b/ibc/store/provable_store_test.go @@ -45,6 +45,12 @@ func TestProvableStore_Get(t *testing.T) { expectedValue: nil, expectedError: coreTypes.ErrIBCKeyDoesNotExist("test/key2"), }, + { + name: "key is nil", + key: nil, + expectedValue: nil, + expectedError: coreTypes.ErrIBCKeyDoesNotExist("test"), + }, } provableStore := newTestProvableStore(t) @@ -344,7 +350,7 @@ func newTestProvableStore(t *testing.T) modules.ProvableStore { require.NoError(t, err) }) - return newProvableStore(bus, []byte("test"), privKey) + return NewProvableStore(bus, []byte("test"), privKey) } func setupDB(t *testing.T) (*smt.SMT, kvstore.KVStore, map[string]string) { diff --git a/runtime/bus.go b/runtime/bus.go index 9d7a6e05f..7052e287f 100644 --- a/runtime/bus.go +++ b/runtime/bus.go @@ -143,6 +143,10 @@ func (m *bus) GetEventLogger() modules.EventLogger { return getModuleFromRegistry[modules.EventLogger](m, modules.EventLoggerModuleName) } +func (m *bus) GetClientManager() modules.ClientManager { + return getModuleFromRegistry[modules.ClientManager](m, modules.ClientManagerModuleName) +} + func (m *bus) GetCurrentHeightProvider() modules.CurrentHeightProvider { return getModuleFromRegistry[modules.CurrentHeightProvider](m, modules.CurrentHeightProviderSubmoduleName) } diff --git a/shared/core/types/error.go b/shared/core/types/error.go index 69369cced..af315a2cc 100644 --- a/shared/core/types/error.go +++ b/shared/core/types/error.go @@ -48,7 +48,7 @@ func NewError(code Code, msg string) Error { } } -// NextCode: 149 +// NextCode: 150 type Code float64 // CONSIDERATION: Should these be a proto enum or a golang iota? //nolint:gosec // G101 - Not hard-coded credentials @@ -198,6 +198,7 @@ const ( CodeIBCStoreAlreadyExistsError Code = 146 CodeIBCStoreDoesNotExistError Code = 147 CodeIBCKeyDoesNotExistError Code = 148 + CodeIBCClientNotActiveError Code = 149 ) const ( @@ -344,6 +345,7 @@ const ( IBCStoreAlreadyExistsError = "ibc store already exists in the store manager" IBCStoreDoesNotExistError = "ibc store does not exist in the store manager" IBCKeyDoesNotExistError = "key does not exist in the ibc store" + IBCClientNotActiveError = "ibc client is not active" ) func ErrUnknownParam(paramName string) Error { @@ -922,3 +924,7 @@ func ErrIBCStoreDoesNotExist(name string) Error { func ErrIBCKeyDoesNotExist(key string) Error { return NewError(CodeIBCKeyDoesNotExistError, fmt.Sprintf("%s: %s", IBCKeyDoesNotExistError, key)) } + +func ErrIBCClientNotActive() Error { + return NewError(CodeIBCClientNotActiveError, IBCClientNotActiveError) +}