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..9d57bc3f8 --- /dev/null +++ b/ibc/client/introspect.go @@ -0,0 +1,151 @@ +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/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 !poktCounter.ProofSpec.ConvertToIcs23ProofSpec().SpecEquals(poktHost.ProofSpec.ConvertToIcs23ProofSpec()) { + 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/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/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/events/event_manager.go b/ibc/events/event_manager.go index 19e48cb95..0b233e139 100644 --- a/ibc/events/event_manager.go +++ b/ibc/events/event_manager.go @@ -1,7 +1,7 @@ package events import ( - coreTypes "github.com/pokt-network/pocket/shared/core/types" + 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" ) @@ -42,14 +42,15 @@ func (*EventManager) Create(bus modules.Bus, options ...modules.EventLoggerOptio func (e *EventManager) GetModuleName() string { return modules.EventLoggerModuleName } -func (e *EventManager) EmitEvent(event *coreTypes.IBCEvent) error { +func (e *EventManager) EmitEvent(event *core_types.IBCEvent) error { wCtx := e.GetBus().GetPersistenceModule().NewWriteContext() defer wCtx.Release() return wCtx.SetIBCEvent(event) } -func (e *EventManager) QueryEvents(topic string, height uint64) ([]*coreTypes.IBCEvent, error) { - rCtx, err := e.GetBus().GetPersistenceModule().NewReadContext(int64(height)) +func (e *EventManager) QueryEvents(topic string, height uint64) ([]*core_types.IBCEvent, error) { + currHeight := e.GetBus().GetConsensusModule().CurrentHeight() + rCtx, err := e.GetBus().GetPersistenceModule().NewReadContext(int64(currHeight)) if err != nil { return nil, err } 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..74ee8d927 --- /dev/null +++ b/ibc/host_introspection_test.go @@ -0,0 +1,55 @@ +package ibc + +import ( + "testing" + "time" + + 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/shared/codec" + "github.com/stretchr/testify/require" +) + +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) +} diff --git a/ibc/main_test.go b/ibc/main_test.go index 51fa0c63f..5dcc083b9 100644 --- a/ibc/main_test.go +++ b/ibc/main_test.go @@ -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 } 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 953e9ca3b..4e5e1bd13 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 c3ff8a171..fb13e74c6 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, @@ -67,8 +67,8 @@ func newProvableStore(bus modules.Bus, prefix coreTypes.CommitmentPrefix, privat // keys are automatically prefixed with the CommitmentPrefix if not present func (p *provableStore) Get(key []byte) ([]byte, error) { prefixed := applyPrefix(p.prefix, key) - currHeight := int64(p.bus.GetConsensusModule().CurrentHeight()) - rCtx, err := p.bus.GetPersistenceModule().NewReadContext(currHeight) + currHeight := p.bus.GetConsensusModule().CurrentHeight() + rCtx, err := p.bus.GetPersistenceModule().NewReadContext(int64(currHeight)) if err != nil { return nil, err } @@ -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 10fdaf3c0..3cb5f808f 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) { @@ -432,7 +438,7 @@ func newPersistenceMock(t *testing.T, EXPECT(). GetIBCStoreEntry(gomock.Any(), gomock.Any()). DoAndReturn( - func(key []byte, _ int64) ([]byte, error) { + func(key []byte, _ uint64) ([]byte, error) { value, ok := dbMap[hex.EncodeToString(key)] if !ok { return nil, coreTypes.ErrIBCKeyDoesNotExist(string(key)) diff --git a/persistence/gov.go b/persistence/gov.go index 5ddec2884..73694ac6e 100644 --- a/persistence/gov.go +++ b/persistence/gov.go @@ -17,6 +17,11 @@ func (p *PostgresContext) GetVersionAtHeight(height int64) (string, error) { return "", nil } +// TODO(#882): Implement this function +func (p *PostgresContext) GetRevisionNumber(height int64) uint64 { + return 1 +} + // TODO: Implement this function func (p *PostgresContext) GetSupportedChains(height int64) ([]string, error) { // This is a placeholder function for the RPC endpoint "v1/query/supportedchains" diff --git a/persistence/ibc.go b/persistence/ibc.go index fc9affea4..0544d7197 100644 --- a/persistence/ibc.go +++ b/persistence/ibc.go @@ -14,14 +14,14 @@ import ( // SetIBCStoreEntry sets the key value pair in the IBC store postgres table at the current height func (p *PostgresContext) SetIBCStoreEntry(key, value []byte) error { ctx, tx := p.getCtxAndTx() - if _, err := tx.Exec(ctx, pTypes.InsertIBCStoreEntryQuery(p.Height, key, value)); err != nil { + if _, err := tx.Exec(ctx, pTypes.InsertIBCStoreEntryQuery(uint64(p.Height), key, value)); err != nil { return err } return nil } // GetIBCStoreEntry returns the stored value for the key at the height provided from the IBC store table -func (p *PostgresContext) GetIBCStoreEntry(key []byte, height int64) ([]byte, error) { +func (p *PostgresContext) GetIBCStoreEntry(key []byte, height uint64) ([]byte, error) { ctx, tx := p.getCtxAndTx() row := tx.QueryRow(ctx, pTypes.GetIBCStoreEntryQuery(height, key)) var valueHex string @@ -50,7 +50,7 @@ func (p *PostgresContext) SetIBCEvent(event *coreTypes.IBCEvent) error { return err } eventHex := hex.EncodeToString(eventBz) - if _, err := tx.Exec(ctx, pTypes.InsertIBCEventQuery(p.Height, typeStr, eventHex)); err != nil { + if _, err := tx.Exec(ctx, pTypes.InsertIBCEventQuery(uint64(p.Height), typeStr, eventHex)); err != nil { return err } return nil diff --git a/persistence/test/ibc_test.go b/persistence/test/ibc_test.go index 2fcf86f4e..c12533ec4 100644 --- a/persistence/test/ibc_test.go +++ b/persistence/test/ibc_test.go @@ -75,7 +75,7 @@ func TestIBC_GetIBCStoreEntry(t *testing.T) { testCases := []struct { name string - height int64 + height uint64 key []byte expectedValue []byte expectedErr error @@ -133,13 +133,12 @@ var ( baseAttributeValue = []byte("testValue") ) -func TestIBCSetEvent(t *testing.T) { +func TestIBC_SetIBCEvent(t *testing.T) { // Setup database db := NewTestPostgresContext(t, 1) // Add a single event at height 1 event := new(coreTypes.IBCEvent) event.Topic = "test" - event.Height = 1 event.Attributes = append(event.Attributes, &coreTypes.Attribute{ Key: baseAttributeKey, Value: baseAttributeValue, @@ -216,7 +215,6 @@ func TestIBCSetEvent(t *testing.T) { db.Height = int64(tc.height) event := new(coreTypes.IBCEvent) event.Topic = tc.topic - event.Height = tc.height for _, attr := range tc.attributes { event.Attributes = append(event.Attributes, &coreTypes.Attribute{ Key: attr.key, @@ -233,7 +231,7 @@ func TestIBCSetEvent(t *testing.T) { } } -func TestGetIBCEvent(t *testing.T) { +func TestIBC_GetIBCEvent(t *testing.T) { // Setup database db := NewTestPostgresContext(t, 1) // Add events "testKey0", "testKey1", "testKey2", "testKey3" @@ -242,10 +240,6 @@ func TestGetIBCEvent(t *testing.T) { for i := 0; i < 4; i++ { event := new(coreTypes.IBCEvent) event.Topic = "test" - event.Height = uint64(i + 1) - if i == 3 { - event.Height = uint64(i) // add a second event at height 3 - } s := strconv.Itoa(i) event.Attributes = append(event.Attributes, &coreTypes.Attribute{ Key: []byte("testKey" + s), @@ -253,8 +247,11 @@ func TestGetIBCEvent(t *testing.T) { }) events = append(events, event) } - for _, event := range events { - db.Height = int64(event.Height) + for i, event := range events { + db.Height = int64(i + 1) + if i == 3 { + db.Height = int64(i) + } require.NoError(t, db.SetIBCEvent(event)) } @@ -301,7 +298,6 @@ func TestGetIBCEvent(t *testing.T) { require.NoError(t, err) require.Len(t, got, tc.expectedLength) for i, index := range tc.eventsIndexes { - require.Equal(t, events[index].Height, got[i].Height) require.Equal(t, events[index].Topic, got[i].Topic) require.Equal(t, events[index].Attributes[0].Key, got[i].Attributes[0].Key) require.Equal(t, events[index].Attributes[0].Value, got[i].Attributes[0].Value) diff --git a/persistence/types/ibc.go b/persistence/types/ibc.go index a783bcf82..7b0d01ee5 100644 --- a/persistence/types/ibc.go +++ b/persistence/types/ibc.go @@ -23,7 +23,7 @@ const ( ) // InsertIBCStoreEntryQuery returns the query to insert a key/value pair into the ibc_entries table -func InsertIBCStoreEntryQuery(height int64, key, value []byte) string { +func InsertIBCStoreEntryQuery(height uint64, key, value []byte) string { return fmt.Sprintf( `INSERT INTO %s(height, key, value) VALUES(%d, '%s', '%s')`, IBCStoreTableName, @@ -34,7 +34,7 @@ func InsertIBCStoreEntryQuery(height int64, key, value []byte) string { } // InsertIBCEventQuery returns the query to insert an event into the ibc_events table -func InsertIBCEventQuery(height int64, topic, eventHex string) string { +func InsertIBCEventQuery(height uint64, topic, eventHex string) string { return fmt.Sprintf( `INSERT INTO %s(height, topic, event) VALUES(%d, '%s', '%s')`, IBCEventLogTableName, @@ -45,7 +45,7 @@ func InsertIBCEventQuery(height int64, topic, eventHex string) string { } // GetIBCStoreEntryQuery returns the latest value for the key at the height provided or at the last updated height -func GetIBCStoreEntryQuery(height int64, key []byte) string { +func GetIBCStoreEntryQuery(height uint64, key []byte) string { return fmt.Sprintf( `SELECT value FROM %s WHERE height <= %d AND key = '%s' ORDER BY height DESC LIMIT 1`, IBCStoreTableName, 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) }