diff --git a/spec/eureka/README.md b/spec/IBC_V2/README.md similarity index 99% rename from spec/eureka/README.md rename to spec/IBC_V2/README.md index 23287f291..62d6eb97a 100644 --- a/spec/eureka/README.md +++ b/spec/IBC_V2/README.md @@ -192,7 +192,6 @@ function sendPacket( // if the sequence doesn't already exist, this call initializes the sequence to 0 sequence = channelStore.get(nextSequenceSendPath(sourcePort, sourceChannel)) - // store commitment to the packet data & packet timeout channelStore.set( packetCommitmentPath(sourcePort, sourceChannel, sequence), diff --git a/spec/IBC_V2/ics-002-client-semantics/README.md b/spec/IBC_V2/ics-002-client-semantics/README.md new file mode 100644 index 000000000..53f87ffc5 --- /dev/null +++ b/spec/IBC_V2/ics-002-client-semantics/README.md @@ -0,0 +1,489 @@ +--- +ics: 2 +title: Client Semantics +stage: draft +category: IBC/TAO +kind: interface +requires: 23, 24 +required-by: 3 +version compatibility: ibc-go v7.0.0 +author: Juwoon Yun , Christopher Goes , Aditya Sripal +created: 2019-02-25 +modified: 2024-08-22 +--- + +## Synopsis + +The IBC protocol provides secure packet flow between applications on different ledgers by verifying the packet messages using clients of the counterparty state machines. While ICS-4 defines the core packet flow logic between two chains and the provable commitments they must make in order to communicate, this standard ICS-2 specifies **how** a chain verifies the IBC provable commitments of the counterparty which is crucial to securely receive and process a packet flow message arriving from the counterparty. + +This standard focuses on how to keep track of the counterparty consensus and verify the state machine; it also specifies the properties that consensus algorithms of state machines implementing the inter-blockchain +communication (IBC) protocol are required to satisfy. +These properties are necessary for efficient and safe verification in the higher-level protocol abstractions. +The algorithm utilised in IBC to verify the state updates of a remote state machine is referred to as a *validity predicate*. +Pairing a validity predicate with a trusted state (i.e., a state that the verifier assumes to be correct), +implements the functionality of a *light client* (often shortened to *client*) for a remote state machine on the host state machine. +In addition to state update verification, every light client is able to detect consensus misbehaviours through a *misbehaviour predicate*. + +Beyond the properties described in this specification, IBC does not impose any requirements on +the internal operation of the state machines and their consensus algorithms. +A state machine may consist of a single process signing operations with a private key (the so-called "solo machine"), a quorum of processes signing in unison, +many processes operating a Byzantine fault-tolerant consensus algorithm (e.g., Tendermint), or other configurations yet to be invented +— from the perspective of IBC, a state machine is defined entirely by its light client validation and misbehaviour detection logic. + +This standard also specifies how the light client's functionality is registered and how its data is stored and updated by the IBC protocol. +The stored client instances can be introspected by a third party actor, +such as a user inspecting the state of the state machine and deciding whether or not to send an IBC packet. + +### Motivation + +The IBC protocol needs to be able to verify updates to the state of another state machine (i.e., the *remote state machine*). +This entails accepting *only* the state updates that were agreed upon by the remote state machine's consensus algorithm. +A light client of the remote state machine is the algorithm that enables the actor to verify state updates of that state machine. +Note that light clients will generally not include validation of the entire state transition logic +(as that would be equivalent to simply executing the other state machine), but may +elect to validate parts of state transitions in particular cases. +This standard formalises the light client model and requirements. +As a result, the IBC protocol can easily be integrated with new state machines running new consensus algorithms, +as long as the necessary light client algorithms fulfilling the listed requirements are provided. + +The IBC protocol can be used to interact with probabilistic-finality consensus algorithms. +In such cases, different validity predicates may be required by different applications. For probabilistic-finality consensus, a validity predicate is defined by a finality threshold (e.g., the threshold defines how many block needs to be on top of a block in order to consider it finalized). +As a result, clients could act as *thresholding views* of other clients: +One *write-only* client could be used to store state updates (without the ability to verify them), +while many *read-only* clients with different finality thresholds (confirmation depths after which +state updates are considered final) are used to verify state updates. + +Client interfaces should also be constructed so that custom validation logic can be provided safely +to define a custom client at runtime, as long as the underlying state machine can provide an +appropriate gas metering mechanism to charge for compute and storage. On a host state machine +which supports WASM execution, for example, the validity predicate and misbehaviour predicate +could be provided as executable WASM functions when the client instance is created. + +### Definitions + +- `Consensus` is a state update generating algorithm. It takes the previous state of a state machine together + with a set of messages (i.e., state machine transactions) and generates a valid state update of the state machine. + Every state machine MUST have a `Consensus` that generates a unique, ordered list of state updates + starting from a genesis state. + + This specification expects that the state updates generated by `Consensus` + satisfy the following properties: + - Every state update MUST NOT have more than one direct successor in the list of state updates. + In other words, the state machine MUST guarantee *finality* and *safety*. + - Every state update MUST eventually have a successor in the list of state updates. + In other words, the state machine MUST guarantee *liveness*. + - Every state update MUST be valid (i.e., valid state transitions). + In other words, `Consensus` MUST be *honest*, + e.g., in the case `Consensus` is a Byzantine fault-tolerant consensus algorithm, + such as Tendermint, less than a third of block producers MAY be Byzantine. + + Unless the state machine satisfies all of the above properties, the IBC protocol +may not work as intended, e.g., users' assets might be stolen. Note that specific client +types may require additional properties. + +- `Height` specifies the order of the state updates of a state machine, e.g., a sequence number. + This entails that each state update is mapped to a `Height`. + +- `ClientMessage` is an arbitrary message defined by the client type that relayers can submit in order to update the client. + The ClientMessage may be intended as a regular update which may add new consensus state for proof verification, or it may contain + misbehaviour which should freeze the client. + +- `ValidityPredicate` is a function that validates a ClientMessage sent by a relayer in order to update the client. + Using the `ValidityPredicate` SHOULD be more computationally efficient than executing `Consensus`. + +```typescript +type ValidityPredicate = (clientState: bytes, trustedConsensusState: bytes, trustedHeight: Number) => (newConsensusState: bytes, newHeight: Number, err: Error) +``` + +- `ConsensusState` is the *trusted view* of the state of a state machine at a particular `Height`. + It MUST contain sufficient information to enable the `ValidityPredicate` to validate future state updates, + which can then be used to generate new `ConsensusState`s. + +- `ClientState` is the state of a client. It MUST expose an interface to higher-level protocol abstractions, + e.g., functions to verify proofs of the existence of particular values at particular paths at particular `Height`s. + +- `MisbehaviourPredicate` is a function that checks whether the rules of `Consensus` were broken, + in which case the client MUST be *frozen*, i.e., no subsequent `ConsensusState`s can be generated. + Verification against the client after it is frozen will also fail. + +```typescript +type MisbehaviourPredicate = (clientState: bytes, trustedConsensusState: bytes, trustedHeight: Number, misbehaviour: bytes) => bool +``` + +- `Misbehaviour` is the proof needed by the `MisbehaviourPredicate` to determine whether + a violation of the consensus protocol occurred. For example, in the case the state machine + is a blockchain, a `Misbehaviour` might consist of two signed block headers with + different `ConsensusState` for the same `Height`. + +### Desired Properties + +Light clients MUST provide state verification functions that provide a secure way +to verify the state of the remote state machines using the existing `ConsensusState`s. +These state verification functions enable higher-level protocol abstractions to +verify sub-components of the state of the remote state machines. + +`ValidityPredicate`s MUST reflect the behaviour of the remote state machine and its `Consensus`, i.e., +`ValidityPredicate`s accept *only* state updates that contain state updates generated by +the `Consensus` of the remote state machine. + +In case of misbehavior, the behaviour of the `ValidityPredicate` might differ from the behaviour of +the remote state machine and its `Consensus` (since clients do not execute the `Consensus` of the +remote state machine). In this case, a `Misbehaviour` SHOULD be submitted to the host state machine, +which would result in the client being frozen. Once the client is frozen, a recovery mechanism to address +the situation must occur before client processing can presume. This recovery mechanism is out-of-scope +of the IBC protocol as the specific recovery needed is highly case-dependent. + +## Technical Specification + +This specification outlines what each *client type* must define. A client type is a set of definitions +of the data structures, initialisation logic, validity predicate, and misbehaviour predicate required +to operate a light client. State machines implementing the IBC protocol can support any number of client +types, and each client type can be instantiated with different initial consensus states in order to track +different consensus instances. + +Specific client types and their specifications are defined in the light clients section of this repository. + +### Data Structures + +#### `Height` + +`Height` is an opaque data structure defined by a client type. +It must form a partially ordered set & provide operations for comparison. + +```typescript +type Height +``` + +```typescript +enum Ord { + LT + EQ + GT +} + +type compare = (h1: Height, h2: Height) => Ord +``` + +A height is either `LT` (less than), `EQ` (equal to), or `GT` (greater than) another height. + +`>=`, `>`, `===`, `<`, `<=` are defined through the rest of this specification as aliases to `compare`. + +There must also be a zero-element for a height type, referred to as `0`, which is less than all non-zero heights. + +#### `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. + +`ConsensusState` MUST be generated from an instance of `Consensus`, which assigns unique heights +for each `ConsensusState` (such that each height has exactly one associated consensus state). +There MUST NOT be two valid `ConensusState`s for the same height. +Such an event is called an "equivocation" and MUST be classified +as misbehaviour. Should one occur, a proof should be generated and submitted so that the client can be frozen +and previous state roots invalidated as necessary. + +```typescript +type ConsensusState = bytes +``` + +The `ConsensusState` MUST define a `getTimestamp()` method which returns the timestamp **in seconds** associated with that consensus state. +This timestamp MUST be the timestamp used in the counterparty state machine and agreed to by `Consensus`. + +```typescript +type getTimestamp = ConsensusState => uint64 +``` + +#### `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. + +Light clients are representation-opaque — different consensus algorithms can define different light client update algorithms — +but they must expose this common set of query functions to the IBC handler. + +```typescript +type ClientState = bytes +``` + +Client types MUST define a method to initialise a client state with the provided client identifier, client state and consensus state, writing to internal state as appropriate. + +```typescript +type initialise = (identifier: Identifier, clientState: ClientState, consensusState: ConsensusState) => Void +``` + +Client types MUST define a method to fetch the current height (height of the most recent validated state update). + +```typescript +type latestClientHeight = ( + clientState: ClientState) + => Height +``` + +Client types MUST define a method on the client state to fetch the timestamp at a given height + +```typescript +type getTimestampAtHeight = ( + clientState: ClientState, + height: Height +) => uint64 +``` + +#### `ClientMessage` + +A `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. + +```typescript +type ClientMessage = bytes +``` + +#### `CommitmentProof` + +`CommitmentProof` is an opaque data structure defined by the client type. + +```typescript +type CommitmentProof = bytes +``` + +It is utilised to verify presence or absence of a particular key/value pair in state +at a particular finalised height (necessarily associated with a particular commitment root). + +### State verification + +Client types must define functions to authenticate internal state of the state machine which the client tracks. +Internal implementation details may differ (for example, a loopback client could simply read directly from the state and require no proofs). + +`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. It MUST return an error if the verification is not successful. +The caller is expected to construct the full `CommitmentPath` from a `CommitmentPrefix` and a standardized path (as defined in [ICS 24](../ics-024-provable-keys/README.md)). + +```typescript +type verifyMembership = ( + clientState: ClientState, + height: Height, + proof: CommitmentProof, + path: CommitmentPath, + value: bytes) + => Error +``` + +`verifyNonMembership` is a generic proof verification method which verifies a proof of absence of a given `CommitmentPath` at the specified height. It MUST return an error if the verification is not successful. +The caller is expected to construct the full `CommitmentPath` from a `CommitmentPrefix` and a standardized path (as defined in [ICS 24](../ics-024-host-requirements/README.md#path-space)). + +Since the verification method is designed to give complete control to client implementations, clients can support chains that do not provide absence proofs by verifying the existence of a non-empty sentinel `ABSENCE` value. Thus in these special cases, the proof provided will be an Existence proof, and the client will verify that the `ABSENCE` value is stored under the given path for the given height. + +```typescript +type verifyNonMembership = ( + clientState: ClientState, + height: Height, + proof: CommitmentProof, + path: CommitmentPath) + => Error +``` + +#### Implementation strategies + +##### Loopback + +A loopback client of a local state machine merely reads from the local state, to which it must have access. + +##### Simple signatures + +A client of a solo state machine with a known public key checks signatures on messages sent by that local state machine, +which are provided as the `Proof` parameter. The `height` parameter can be used as a replay protection nonce. + +Multi-signature or threshold signature schemes can also be used in such a fashion. + +##### Proxy clients + +Proxy clients verify another (proxy) state machine's verification of the target state machine, by including in the +proof first a proof of the client state on the proxy state machine, and then a secondary proof of the sub-state of +the target state machine with respect to the client state on the proxy state machine. This allows the proxy client to +avoid storing and tracking the consensus state of the target state machine itself, at the cost of adding +security assumptions of proxy state machine correctness. + +##### Merklized state trees + +For clients of state machines with Merklized state trees, these functions can be implemented as MerkleTree Existence and NonExistence proofs. Client implementations may choose to implement these methods for the specific tree used by the counterparty chain or they can use the tree-generic [ICS-23](github.com/cosmos/ics23) `verifyMembership` or `verifyNonMembership` methods, using a verified Merkle +root stored in the `ClientState`, to verify presence or absence of particular key/value pairs in state at particular heights for any ICS-23 compliant tree given a ProofSpec that describes how the tree is constructed. In this case, the ICS-23 `ProofSpec` MUST be provided to the client on initialization. + +```typescript +type verifyMembership = (ClientState, Height, CommitmentProof, Path, Value) => boolean +``` + +```typescript +type verifyNonMembership = (ClientState, Height, CommitmentProof, Path) => boolean +``` + +ProofVerification Inputs: +- `clientId: bytes`: The identifier of the client that will verify the proof. +- `Height: Number`: The height for the consensus state that the proof will be verified against. +- `Path: CommitmentPath`: The path of the key being proven. In the IBC protocol, this will be an ICS24 standardized path prefixed by the `CommitmentPrefix` registered on the counterparty. The `Path` MUST be constructed by the IBC handler given the IBC message, it MUST NOT be provided by the relayer as the relayer is untrusted. +- `Value: Optional`: The value being proven. If it is non-empty this is a membership proof. If the value is nil, this is a non-membership proof. + +ProofVerification Preconditions: +- A client has already been created for the `clientId`. +- A `ConsensusState` is stored for the given `Height`. + +ProofVerification Postconditions: +- Proof verification should be stateless in most cases. In the case that the proof verification is a signature check, we may wish to increment a nonce to prevent replay attacks. + +ProofVerification Errorconditions: +- `CommitmentProof` does not successfully verify with the provided `CommitmentPath` and `Value` with the retrieved `ConsensusState` for the provided `Height`. + +### Sub-protocols + +IBC handlers MUST implement the functions defined below. + +#### Identifier validation + +Clients are stored under a unique `Identifier` prefix. +This ICS does not require that client identifiers be generated in a particular manner, only that they be unique. +However, it is possible to restrict the space of `Identifier`s if required. +The validation function `validateClientIdentifier` MAY be provided. + +```typescript +type validateClientIdentifier = (id: Identifier) => boolean +``` + +If not provided, the default `validateClientIdentifier` will always return `true`. + +##### Utilising past roots + +To avoid race conditions between client updates (which change the state root) and proof-carrying +transactions in handshakes or packet receipt, many IBC handler functions allow the caller to specify +a particular past root to reference, which is looked up by height. IBC handler functions which do this +must ensure that they also perform any requisite checks on the height passed in by the caller to ensure +logical correctness. + +#### CreateClient + +Calling `createClient` with the client state and initial consensus state creates a new client. The intiator of this client is responsible for setting all of the initial parameters of the `ClientState` and the initial root-of-trust `ConsensusState`. The client implementation is then responsible for executing the light client `ValidityPredicate` against these initial parameters. Thus, once a root-of-trust is instantiated; the light client guarantees to preserve that trust within the confines of the security model as parameterized by the `ClientState`. If a user verifies that a client is a valid client of the counterparty chain once, they can be guaranteed that it will remain a valid client into the future so long as the `MisbehaviourPredicate` is not triggered. If the `MisbehaviourPredicate` is triggered however, this can be submitted as misbehaviour to freeze the IBC light client operations. + +CreateClient Inputs: +`clientType: string`: This is the client-type that references a particular light client implementation on the chain. The `CreateClient` message will create a new instance of the given client-type. +`ClientState: bytes`: This is the opaque client state as defined for the given client type. It will contain any parameters needed for verifying client updates and proof verification against a `ConsensusState`. The `ClientState` parameterizes the security model as implemented by the client type. +`ConsensusState: bytes`: This is the opaque consensus state as defined for the given client type. It is the initial consensus state provided and MUST be capable of being used by the `ValidityPredicate` to add new `ConsensusState`s to the client. The initial `ConsensusState` MAY also be used for proof verification but it is not necessary. +`Height: Number`: This is the height that is associated with the initial consensus state. + +CreateClient Preconditions: +- The provided `clientType` is supported by the chain and can be routed to by the IBC handler. + +CreateClient PostConditions: +- A unique identifier `clientId` is generated for the client +- The provided `ClientState` is persisted to state and retrievable given the `clientId`. +- The provided `ConsensusState` is persisted to state and retrievable given the `clientId` and `height`. + +CreateClient ErrorConditions: +- The provided `ClientState` is invalid given the client type. +- The provided `ConsensusState` is invalid given the client type. +- The `Height` is not a positive number. + +#### RegisterCounterparty + +IBC Version 2 introduces a `registerCounterparty` procedure. Calling `registerCounterparty` with the clientId will register the counterparty clientId +that the counterparty will use to write packet messages intended for our chain. All ICS24 provable paths to our chain will be keyed on the counterparty clientId, so each client must be aware of the counterparty's identifier in order to construct the path for key verification and ensure there is an authenticated stream of packet data between the clients that do not get written to by other clients. +The `registerCounterparty` also includes the `CommitmentPrefix` to use for the counterparty chain. Most chains will not store the ICS24 directly under the root of a MerkleTree and will instead store the standardized paths under a custom prefix, thus the counterparty client must be given this information to verify proofs correctly. The `CommitmentPrefix` is defined as an array of byte arrays to support nested Merkle trees. In this case, each element of the outer array is a key for each tree in the nested structure ordered from the top-most tree to the lowest level tree. In this case, the ICS24 path is appended to the key of the lowest-level tree (i.e. the last element of the commitment prefix) in order to get the full `CommitmentPath` for proof verification. + +RegisterCounterparty Inputs: +`clientId: bytes`: The clientId on the executing chain. +`counterpartyClientId: bytes`: The identifier of the client used by the counterparty chain to verify the executing chain. +`counterpartyCommitmentPrefix: []bytes`: The prefix used by the counterparty chain. + +RegisterCounterparty Preconditions: +- A client has already been created for the `clientId` + +RegisterCounterparty Postconditions: +- The `counterpartyClientId` is retrievable given the `clientId`. +- The `counterpartyCommitmentPrefix` is retrievable given the `clientId`. + +RegisterCounterparty ErrorConditions: +- There does not exist a client for the given `clientId` +- `RegisterCounterparty` has already been called for the given `clientId` + +NOTE: Once the clients and counterparties have been registered on both sides, the connection between the clients is established and packet flow between the clients may commence. Users are expected to verify that the clients and counterparties are set correctly before using the connection to send packets. They may do this directly themselves or through social consensus. +NOTE: `RegisterCounterparty` is setting information that will be crucial for proper proof verification of IBC messages using our client. Thus, it must be authenticated properly. The `RegisterCounterparty` message can be permissionless in which case the fields must be authenticated against the counterparty chain using the client which may prove difficult and cumbersome. It is RECOMMENDED to simply ensure that the client creator address is the same as the one that registers the counterparty. Once the client and counterparty are set by the same creator, users can decide if the configuration is secure out-of-band. + +#### Update + +Updating a client is done by submitting a new `ClientMessage`. The `Identifier` is used to point to the +stored `ClientState` that the logic will update. When a new `ClientMessage` is verified using the `ValidityPredicate` with +the stored `ClientState` and a previously stored `ConsensusState`, the client MUST then add a new `ConsensusState` with a new `Height`. + +If a client can no longer be updated (if, for example, the trusting period has passed), +then new packet flow will not be able to be processed. Manual intervention must take place to +reset the client state or migrate the client. This +cannot safely be done completely automatically, but chains implementing IBC could elect +to allow governance mechanisms to perform these actions +(perhaps even per-client/connection/channel in a multi-sig or contract). + +UpdateClient Inputs: +`clientId: bytes`: The identifier of the client being updated. +`clientMessage: bytes`: The opaque clientMessage to update the client as defined by the given `clientType`. It MUST include the `trustedHeight` we wish to update from. This `trustedHeight` will be used to retrieve a trusted ConsensusState which we will use to update to a new consensus state using the `ValidityPredicate`. + +UpdateClient Preconditions: +- A client has already been created for the `clientId` + +UpdateClient Postconditions: +- A new `ConsensusState` is added to the client and persisted with a new `Height` +- Implementations MAY automatically detect misbehaviour in `UpdateClient` if the update itself is proof of misbehaviour (e.g. There is already a different `ConsensusState` for the given height, or time monotonicity is broken). It is recommended to automatically freeze the client in this case to avoid having to send a redundant `submitMisbehaviour` message. + +UpdateClient ErrorConditions: +- The trusted `ConsensusState` referenced in the `ClientMessage` does not exist in state +- `ValidityPredicate(clientState, trustedConsensusState, trustedHeight)` returns an error + +#### Misbehaviour + +If `Consensus` of the counterparty chain is violated, then the relayer can submit proof of this as misbehaviour. Once the client is frozen, no updates may take place and all proof verification will fail. The client may be unfrozen by an out-of-band protocol once trust in the counterparty `Consensus` is restored and any invalid state caused by the break in `Consensus` is reverted on the executing chain. + +SubmitMisbehaviour Inputs: +`clientId: bytes`: The identifier of the client being frozen. +`clientMessage: bytes`: The opaque clientMessage to freeze the client as defined by the given `clientType`. It MUST include the `trustedHeight` we wish to verify misbehaviour from. This `trustedHeight` will be used to retrieve a trusted ConsensusState which we will use to freeze the client given the `MisbehaviourPredicate`. It MUST also include the misbehaviour being submitted. + +SubmitMisbehaviour Preconditions: +- A client has already been created for the `clientId`. + +SubmitMisbehaviour Postconditions: +- The client is frozen, update and proof verification will fail until client is unfrozen again. + +SubmitMisbehaviour ErrorConditions: +- The trusted `ConsensusState` referenced in the `ClientMessage` does not exist in state. +- `MisbehaviourPredicate(clientState, trustedConsensusState, trustedHeight, misbehaviour)` returns `false`. + +### Properties & Invariants + +- Client identifiers are immutable & first-come-first-serve. Clients cannot be deleted (allowing deletion would potentially allow future replay of past packets if identifiers were re-used). + +## Backwards Compatibility + +Not applicable. + +## Forwards Compatibility + +New client types can be added by IBC implementations at-will as long as they conform to this interface. + +## Example Implementations + +Please see the ibc-go implementations of light clients for examples of how to implement your own: . + +## History + +Mar 5, 2019 - Initial draft finished and submitted as a PR + +May 29, 2019 - Various revisions, notably multiple commitment-roots + +Aug 15, 2019 - Major rework for clarity around client interface + +Jan 13, 2020 - Revisions for client type separation & path alterations + +Jan 26, 2020 - Addition of query interface + +Jul 27, 2022 - Addition of `verifyClientState` function, and move `ClientState` to the `provableStore` + +August 4, 2022 - Changes to ClientState interface and associated handler to align with changes in 02-client-refactor ADR: + +August 22, 2024 - [Changes for IBC/TAO V2](https://github.com/cosmos/ibc/pull/1147) + +## Copyright + +All content herein is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/spec/IBC_V2/ics-004-channel-and-packet-semantics/PACKET.md b/spec/IBC_V2/ics-004-channel-and-packet-semantics/PACKET.md new file mode 100644 index 000000000..3b7df4e3f --- /dev/null +++ b/spec/IBC_V2/ics-004-channel-and-packet-semantics/PACKET.md @@ -0,0 +1,170 @@ +# Packet Structure and Provable Commitment Specification + +## Packet V2 Structure + +The IBC packet sends application data from a source chain to a destination chain with a timeout that specifies when the packet is no longer valid. The packet will be committed to by the source chain as specified in the ICS-24 specification. The receiver chain will then verify the packet commitment under the ICS-24 specified packet commitment path. If the proof succeeds, the IBC handler sends the application data(s) to the relevant application(s). + +```typescript +interface Packet { + // identifier for the destination-chain client existing on source chain + sourceClientId: bytes, + // identifier for the source-chain client existing on destination chain + destClientId: bytes, + // the sequence uniquely identifies this packet + // in the stream of packets from source to dest chain + sequence: uint64, + // the timeout is the timestamp in seconds on the destination chain + // at which point the packet is no longer valid. + // It cannot be received on the destination chain and can + // be timed out on the source chain + timeout: uint64, + // the data includes the messages that are intended + // to be sent to application(s) on the destination chain + // from application(s) on the source chain + // IBC core handlers will route the payload to the desired + // application using the port identifiers but the rest of the + // payload will be processed by the application + data: [Payload] +} + +interface Payload { + // sourcePort identifies the sending application on the source chain + sourcePort: bytes, + // destPort identifies the receiving application on the dest chain + destPort: bytes, + // version identifies the version that sending application + // expects destination chain to use in processing the message + // if dest chain does not support the version, the payload must + // be rejected with an error acknowledgement + version: string, + // encoding allows the sending application to specify which + // encoding was used to encode the app data + // the receiving applicaton will decode the appData into + // the strucure expected given the version provided + // if the encoding is not supported, receiving application + // must be rejected with an error acknowledgement. + // the encoding string MUST be in MIME format + encoding: string, + // appData is the opaque content sent from the source application + // to the dest application. It will be decoded and interpreted + // as specified by the version and encoding fields + appData: bytes, +} +``` + +The source and destination client identifiers at the top-level of the packet identify the chains communicating. The `sourceClientId` identifier **must** be unique on the source chain and is a pointer to the destination chain client on the source chain. The `destClientId` identifier **must** be a unique identifier on the destination chain and is a pointer to the source chain client on the destination chain. The sequence is a monotonically incrementing nonce to uniquely identify packets sent between the source and destination chain. + +The timeout is the UNIX timestamp in seconds that must be passed on the **destination** chain before the packet is invalid and no longer capable of being received. Note that the timeout timestamp is assessed against the destination chain's clock which may drift relative to the clocks of the sender chain or a third party observer. If a packet is received on the destination chain after the timeout timestamp has passed relative to the destination chain's clock; the packet must be rejected so that it can be safely timed out and reverted by the sender chain. + +In version 2 of the IBC specification, implementations **MAY** support multiple application data within the same packet. This can be represented by a list of payloads. Implementations may choose to only support a single payload per packet, in which case they can just reject incoming packets sent with multiple payloads. + +Each payload will include its own `Encoding` and `AppVersion` that will be sent to the application to instruct it how to decode and interpret the opaque application data. The application must be able to support the provided `Encoding` and `AppVersion` in order to process the `AppData`. If the receiving application does not support the encoding or app version, then the application **must** return an error to IBC core. If the receiving application does support the provided encoding and app version, then the application must decode the application as specified by the `Encoding` string and then process the application as expected by the counterparty given the agreed-upon app version. Since the `Encoding` and `AppVersion` are now in each packet they can be changed on a per-packet basis and an application can simultaneously support many encodings and app versions from a counterparty. This is in stark contrast to IBC version 1 where the channel prenegotiated the channel version (which implicitly negotiates the encoding as well); so that changing the app version after channel opening is very difficult. + +All implementations must commit the packet in the standardized IBC commitment format to satisfy the protocol. In order to do this we must first commit the packet data and timeout. The timeout is encoded in LittleEndian format. The packet data which is a list of payloads is committed to by hashing each individual field of the payload and successively concatenating them together. This ensures a standard unambigious commitment for a given packet. Thus a given packet will always create the exact same commitment by all compliant implementations and two different packets will never create the same commitment by a compliant implementation. This commitment value is then stored under the standardized provable packet commitment key as defined below: + +```typescript +func packetCommitmentPath(packet: Packet): bytes { + return packet.sourceClientId + byte(0x01) + bigEndian(packet.sequence) +} +``` + +```typescript +// commitPayload hashes all the fields of the packet data to create a standard size +// preimage before committing it in the packet. +func commitPayload(payload: Payload): bytes { + buffer = sha256.Hash(payload.sourcePort) + buffer = append(sha256.Hash(payload.destPort)) + buffer = append(sha256.Hash(payload.version)) + buffer = append(sha256.Hash(payload.encoding)) + buffer = append(sha256.Hash(payload.appData)) + return sha256.Hash(buffer) +} + +// commitV2Packet commits to all fields in the packet +// by hashing each individual field and then hashing these fields together +// Note: SourceClient and the sequence are omitted since they will be included in the key +// Every other field of the packet is committed to in the packet which will be stored in the +// packet commitment value +// The final preimage will be prepended by the byte 0x02 before hashing in order to clearly define the protocol version +// and allow for future upgradability +func commitV2Packet(packet: Packet) { + timeoutBytes = LittleEndian(packet.timeout) + var appBytes: bytes + for p in packet.payload { + appBytes = append(appBytes, commitPayload(p)) + } + buffer = sha256.Hash(packet.destClient) + buffer = append(buffer, sha256.hash(timeoutBytes)) + buffer = append(buffer, sha256.hash(appBytes)) + buffer = append([]byte{0x02}, buffer) + return sha256.Hash(buffer) +} +``` + +## Acknowledgement V2 + +The acknowledgement in the version 2 specification is also modified to support multiple payloads in the packet that will each go to separate applications that can write their own acknowledgements. Each acknowledgment will be contained within the final packet acknowledgment in the same order that they were received in the original packet. Thus if a packet contains payloads for modules `A` and `B` in that order; the receiver will write an acknowledgment with the app acknowledgements `A` and `B` in the same order. + +The acknowledgement which is itself a list of app acknowledgement bytes must be committed to by hashing each individual acknowledgement and concatenating them together and hashing the result. This ensures that all compliant implementations reach the same acknowledgment commitment and that two different acknowledgements never create the same commitment. + +An application may not need to return an acknowledgment. In this case, it may return a sentinel acknowledgement value `SENTINEL_ACKNOWLEDGMENT` which will be the single byte in the byte array: `bytes(0x01)`. In this case, the IBC `acknowledgePacket` handler will still do the core IBC acknowledgment logic but it will not call the application's acknowledgePacket callback. + +```typescript +interface Acknowledgement { + // Each app in the payload will have an acknowledgment in this list in the same order + // that they were received in the payload + // If an app does not need to send an acknowledgement, there must be a SENTINEL_ACKNOWLEDGEMENT + // in its place + // The app acknowledgement must be encoded in the same manner specified in the payload it received + // and must be created and processed in the manner expected by the version specified in the payload. + appAcknowledgement: [bytes] +} +``` + +All acknowledgements must be committed to and stored under the standardized acknowledgment path. Note that since each acknowledgement is associated with a given received packet, the acnowledgement path is constructed using the packet `destClientId` and its `sequence` to generate a unique key for the acknowledgement. + +```typescript +func acknowledgementPath(packet: Packet) { + return packet.destClientId + byte(0x02) + bigEndian(packet.Sequence) +} +``` + +```typescript +// commitV2Acknowledgement hashes each app acknowledgment and hashes them together +// the final preimage will be prepended with the byte 0x02 before hashing in order to clearly define the protocol version +// and allow for future upgradability +func commitV2Acknowledgment(ack: Acknowledgement) { + var buffer: bytes + for appAck in ack.appAcknowledgement { + buffer = append(buffer, sha256.Hash(appAck)) + } + buffer = append([]byte{0x02}, buffer) + return sha256.Hash(buffer) +} +``` + +## Packet Receipt V2 + +A packet receipt will only tell the sending chain that the counterparty has successfully received the packet. Thus we just need a provable boolean flag uniquely associated with the sent packet. Thus, the receiver chain stores the packet receipt keyed on the destination identifier and the sequence to uniquely identify the packet. + +For chains that support nonexistence proofs of their own state, they can simply write a `SENTINEL_RECEIPT_VALUE` under the receipt path. This `SENTINEL_RECEIPT_PATH` can be any non-nil value so it is recommended to write a single byte. The receipt path is standardized as below. Similar to the acknowledgement, each receipt is associated with a given received packet the receipt path is constructed using the packet `destClientId` and its `sequence` to generate a unique key for the receipt. + +```typescript +func receiptPath(packet: Packet) { + return packet.destClientId + byte(0x03) + bigEndian(packet.Sequence) +} +``` + +## Provable Path-space + +IBC/TAO implementations MUST implement the following paths for the `provableStore` in the exact format specified. This is because counterparty IBC/TAO implementations will construct the paths according to this specification and send it to the light client to verify the IBC specified value stored under the IBC specified path. The `provableStore` is specified in [ICS24 Host Requirements](../ics-024-provable-keys/README.md) + +Future paths may be used in future versions of the protocol, so the entire key-space in the provable store MUST be reserved for the IBC handler. + +| Value | Path format | Value type | +| -------------------------- | ---------------------------------------------- | ---------- | +| Packet Commitment | {sourceClientId}|0x1|{bigEndianUint64Sequence} | bytes | +| Packet Receipt | {destClientId}|0x2|{bigEndianUint64Sequence} | bytes | +| Acknowledgement Commitment | {destClientId}|0x3|{bigEndianUint64Sequence} | bytes | + +Note that the IBC protocol ensures that the packet `(sourceClientId, sequence)` tuple uniquely identifies a packet on the sending chain, and the `(destClientId, sequence)` tuple uniquely identifies a packet on the receiving chain. This property along with the byte separator between the client identifier and sequence in the standardized paths ensures that commitments, receipts, and acknowledgements are each written to different paths for the same packet. Thus, so long as the host requirements specified in ICS24 are respected; a provable key written to state by the IBC handler for a given packet will never be overwritten with a different value. This ensures secure and correct communication between chains in the IBC ecosystem. diff --git a/spec/IBC_V2/ics-004-channel-and-packet-semantics/PACKET_HANDLER.md b/spec/IBC_V2/ics-004-channel-and-packet-semantics/PACKET_HANDLER.md new file mode 100644 index 000000000..744c505b8 --- /dev/null +++ b/spec/IBC_V2/ics-004-channel-and-packet-semantics/PACKET_HANDLER.md @@ -0,0 +1,180 @@ +# IBC Packet Handler + +The packet handler specification defines the semantics and behavior that implementations must enforce in order to support IBC v2 protocol. + +### Packet Structure + +A `Packet` in the interblockchain communication protocol is the primary interface by which applications will send data to counterparty applications on other chains. It is defined as follows: + +```typescript +interface Packet { + sourceClientId: bytes // identifier of the client on the sending chain + destClientId: bytes // identifier of the client on the receiving chain + sequence: uint64 // unique number identifying this packet in the stream of packets from sourceClientId to destClientId + timeoutTimestamp: uint64, // indicates the timeout as a UNIX timestamp in seconds. If the timeout timestamp is reached on destination chain, it is no longer receivable + data: Payload[] // a list of payloads intended for applications on the receiving chain +} +``` + +```typescript +interface Payload { + sourcePort: bytes, // identifier of the sending application on the sending chain + destPort: bytes, // identifier of the receiving application on the receiving chain + version: string, // payload version only interpretable by sending/receiving applications + encoding: string, // payload encoding only interpretable by sending/receiving applications + value: bytes // application-specific data that can be parsed by receiving application given the version and encoding +} +``` + +The packet is never directly serialised and sent to counterparty chains. Instead a standardized non-malleable committment to the packet data is stored under the standardized unique key for the packet as defined in ICS-24. Thus, implementations MAY make individual choices on the exact packet structure and serialization scheme they use internally so long as they respect the standardized commitment defined by the IBC protocol when writing to the provable store. + +Packet Invariants: +- None of the packet fields are allowed to be empty +- For every payload included, none of the payload fields are allowed to be empty + +### Receipt + +A `Receipt` is a sentinel byte that is stored under the standardized provable ReceiptPath of a given packet by the receiving chain when it successfully receives the packet. This prevents replay attacks and also the possibility of timing out a packet on the sender chain when the packet has already been received. The specific value of the receipt does not matter so long as its not empty. + +### Acknowledgement Structure + +An `Acknowledgement` is the interface that will be used by receiving applications to return application specific information back to the sender. If every application successfully received its payload, then each receiving application will return their custom acknowledgement bytes which will be appended to the acknowledgement array. If **any** application returns an error, then the acknowledgement will have a single element with a sentinel error acknowledgement. + +```typescript +const ErrorAcknowledgement = sha256("UNIVERSAL_ERROR_ACKNOWLEDGEMENT") + +interface Acknowledgement { + appAcknowledgement bytes[] // array of an array of bytes. Each element of the array contains an acknowledgement from a specific application +} +``` + +Acknowledgement Invariants: +- If the acknowledgement interface includes an error acknowledgement then there must be only a single element in the array with the error acknowledgement +- There CANNOT be multiple app acknowledgements where an element is the error acknowledgement +- If there are multiple app acknowledgements, the length of the app acknowledgements is the same length as the payloads in the associated packet and each acknowledgement is associated with the payload in the same position in the payload array. + +### SendPacket + +SendPacket is called by users to execute an inter-blockchain flow. The user submits a message with a payload(s) for each IBC application they wish to interact with. The SendPacket handler must call the sendPacket logic of each sending application as identified by the sourcePort of the payload. If none of the sending applications error, then the sendPacket handler must construct the packet with the user-provided sourceClient, payloads, and timeout and the destinationClient it retrieves from the counterparty storage given the sourceClient and a generated sequence that is unique for the sourceClientId. It will commit the packet with the ICS24 commitment function under the ICS24 path. The sending chain MAY store the ICS24 path under a custom prefix in the provable store. In this case, the counterparty must have knowledge of the custom prefix as provided by the relayer on setup. The sending chain SHOULD check the provided timestamp against an authenticated time oracle (local BFT time or destination client latest timestamp) and preemptively reject a user-provided packet with a timestamp that has already passed. + +The user may be an off-chain process or an on-chain actor. In either case, the user is not trusted by the IBC protocol. The IBC application is responsible for properly authenticating that the user is allowed to send the requested app data using the IBC application's port as specified in the source port of the payload. The IBC application is also responsible for executing any app-specific logic that must run before the IBC packet can be sent (e.g. escrowing user's tokens before sending a fungible token transfer packet). + +SendPacket Inputs: +`payload: Payload[]`: List of payloads that are to be sent from source applications on sending chain to corresponding destination applications on the receiving chain. Implementations MAY choose to only support a single payload per packet. +`sourceClientId: bytes`: Identifier of the receiver chain client that exists on the sending chain. +`timeoutTimestamp: uint64`: The timeout in UNIX seconds after which the packet is no longer receivable on the receiving chain. NOTE: This timestamp is evaluated against the **receiving chain** clock as there may be drift between the sending chain and receiving chain clocks + +SendPacket Preconditions: +- A valid client exists on the sending chain with the `sourceClientId` +- There exists a mapping on the sending chain from `sourceClientId` to `Counterparty` + +SendPacket Postconditions: +- The sending application(s) as identified by the source port(s) in the payload(s) have all executed their sendPacket logic successfully +- The following packet gets committed and stored under the packet commitment path as specified by ICS24: +```typescript +interface Packet { + sourceClientId: msg.sourceClientId, + destClientId: counterparty.ClientId, + sequence: generateUniqueSequence(sourceClientId), + timeoutTimestamp: msg.timeoutTimestamp + data: msg.Payloads +} +``` +- Since the packet is committed to with a hash in-state, implementations must provide the packet fields for relayers to reconstruct. This can be emitted in an event system or stored in state as the full packet under an auxilliary key if the implementing platform does not have an event system. + +SendPacket Errorconditions: +- Any of the sending applications returns an error during its sendPacket logic execution +- The sending client is invalid (expired or frozen) + +SendPacket Invariants: +- The sourceClientId MUST exist on the sending chain +- The destClientId MUST be the registered counterparty of the sourceClientId on the sending chain +- The sending chain MUST NOT have sent a previous packet with the same `sourceClientId` and `sequence` + +### RecvPacket + +RecvPacket is called by relayers once a packet has been committed on the sender chain in order to process the packet on the receiving chain. Since the relayer is not trusted, the relayer must provide a proof that the sender chain had indeed committed the provided packet which will be verified against the `destClient` on the receiving chain. + +If the proof succeeds, and the packet passes replay and timeout checks; then each payload is sent to the receiving application as part of the receiving application callback. + +RecvPacket Inputs: +`packet: Packet`: The packet sent from the sending chain to our chain +`proof: bytes`: An opaque proof that will be sent to the destination client. The destination client is responsible for interpreting the bytes as a proof and verifying the packet commitment key/value provided by the packet handler against the provided proof. +`proofHeight: Number`: This is the height of the counterparty chain from which the proof was generated. A corresponding consensus state for this height must exist on the destination client for the proof to verify correctly. + +RecvPacket Preconditions: +- A valid client exists on the receiving chain with `destClientId` +- There exists a mapping from `destClientId` to `Counterparty` + +RecvPacket Postconditions: +- A packet receipt is stored under the specified ICS24 with the `destClientId` and `sequence` +- All receiving application(s) as identified by the destPort(s) in the payload(s) have executed their recvPacket logic + +RecvPacket Errorconditions: +- `Counterparty.ClientId` != `packet.sourceClientId` ensures that packet was sent by expected counterparty +- `packet.TimeoutTimestamp` >= `chain.BlockTime()` ensures we cannot receive successfully if packet can be timed out on sending chain +- Packet receipt does not already exist in state for the `destClientId` and `sequence`. This prevents replay attacks +- Membership proof does not successfully verify + +### WriteAcknowledgement + +WriteAcknowledgement Inputs: +`destClientId: bytes`: Identifier of the sender chain client that exist on the receiving chain +`sequence: uint64`: Unique sequence identifying the packet from sending chain to receiving chain +`ack: Acknowledgement`: Acknowledgement collected by receiving chain from all receiving applications after they have returned their individual acknowledgement. If any individual application errors, the entire acknowledgement MUST have a single element with just the SENTINEL ERROR ACKNOWLEDGEMENT. If all applications successfully received, then every application must have its own acknowledgement set in the `Acknowledgement` in the same order that they existed in the payload of the sending packet. + +WriteAcknowledgement Preconditions: +- A packet receipt is stored under the specified ICS24 with the `destClientId` and `sequence` +- An acknowledgement for the `destClientId` and `sequence` has not already been written under the ICS24 path + +WriteAcknowledgement Postconditions: +- The acknowledgement is committed and written to the acknowledgement path as specified in ICS24 +- Since the acknowledgement is being hashed, the full acknowledgement fields should be made available for relayers to reconstruct. This can be emitted in an event system or stored in state as the full packet under an auxilliary key if the implementing platform does not have an event system. +- Implementors SHOULD also emit the full packet again in `WriteAcknowledgement` since the sender chain is only expected to store the packet commitment and not the full packet; relayers are expected to pass the packet back to the sender chain to process the acknowledgement. Thus, in order to support stateless relayers it is helpful to re-emit the packet fields on `WriteAcknowledgement` so the relayer can reconstruct the packet. +- If the acknowledgement is successful, then all receiving applications must have executed their recvPacket logic and written state +- If the acknowledgement is unsuccessful (ie ERROR ACK), any state changes made by the receiving applications MUST all be reverted. This ensure atomic execution of the multi-payload packet. + +### AcknowledgePacket + +AcknowledgePacket Inputs: +`packet: Packet`: The packet that was originally sent by our chain +`acknowledgement: Acknowledgement`: The acknowledgement written by the receiving chain for the packet +`proof: bytes`: An opaque proof that will be sent to the source client. The source client is responsible for interpreting the proof and verifying it against the acknowledgement key/value provided by the packet handler. +`proofHeight: Number`: This is the height of the counterparty chain from which the proof was generated. A corresponding consensus state for this height must exist on the source client for the proof to verify correctly. + +AcknowledgePacket Preconditions: +- A valid client exists on the sending chain with the `sourceClientId` +- There exists a mapping on the sending chain from `sourceClientId` to `Counterparty` +- A packet commitment has been stored under the ICS24 packet path with `sourceClientId` and `sequence` + +AcknowledgePacket Postconditions: +- All sending applications execute the ackPacket logic with the payload and the individual acknowledgement for that payload or the universal `ErrorAcknowledgement`. +- Stored commitment for the packet is deleted + +AcknowledgePacket Errorconditions: +- `packet.destClient` != `counterparty.ClientId`. This should never happen if the second error condition is not true, since we constructed the packet correctly earlier +- The packet provided by the relayer does not commit to the stored commitment we have stored for the `sourceClientId` and `sequence` +- Membership proof of the acknowledgement commitment on the receiving chain as standardized by ICS24 does not verify + +### TimeoutPacket + +TimeoutPacket Inputs: +`packet: Packet`: The packet that was originally sent by our chain +`proof: bytes`: An opaque non-existence proof that will be sent to the source client. The source client is responsible for interpreting the proof and verifying it against the receipt key provided by the packet handler. +`proofHeight: Number`: This is the height of the counterparty chain from which the proof was generated. A corresponding consensus state for this height must exist on the source client for the proof to verify correctly. + +TimeoutPacket Preconditions: +- A valid client exists on the sending chain with the `sourceClientId` +- There exists a mapping on the sending chain from `sourceClientId` to `Counterparty` +- A packet commitment has been stored under the ICS24 packet path with `sourceClientId` and `sequence` + +TimeoutPacket Postconditions: +- All sending applications execute the timeoutPacket logic with the payload. +- Stored commitment for the packet is deleted + +TimeoutPacket Errorconditions: +- `packet.destClient` != `counterparty.ClientId`. This should never happen if the second error condition is not true, since we constructed the packet correctly earlier +- The packet provided by the relayer does not commit to the stored commitment we have stored for the `sourceClientId` and `sequence` +- Non-Membership proof of the packet receipt on the receiving chain as standardized by ICS24 does not verify + + diff --git a/spec/IBC_V2/ics-005-port-allocation/README.md b/spec/IBC_V2/ics-005-port-allocation/README.md new file mode 100644 index 000000000..c89ad7f42 --- /dev/null +++ b/spec/IBC_V2/ics-005-port-allocation/README.md @@ -0,0 +1,54 @@ +--- +ics: 5 +title: Port Allocation +stage: Draft +required-by: 4 +category: IBC/TAO +kind: interface +version compatibility: ibc-go v10.0.0 +author: Aditya Sripal +created: 2024-05-17 +--- + +## Synopsis + +This standard specifies the port allocation system by which modules can bind to uniquely named ports allocated by the IBC handler. +The port identifiers in the packet defines which application to route the packet callback to. The source portID is an identifier of the application sending the packet, thus it will also receive the `AcknowledgePacket` and `TimeoutPacket` callback. The destination portID is the identifier of the application receiving the packet and will receive the `ReceivePacket` callback. + +Modules may register multiple ports on a state machine and send from any of their registered ports to any arbitrary port on a remote state machine. Each port on a state machine must be mapped to a specific IBC module as defined by [ICS-26](../ics-026-application-callbacks/README.md). Thus the IBC application to portID mapping is one-to-many. + +NOTE: IBC v1 included a channel along with a channel handshake that explicitly associated a unique channel between two portIDs on counterparty chains. Thus, the portIDs on both sides were tightly coupled such that no other application other than the ones bound by the portIDs were allowed to send packets on the dedicated channel. IBC v2 removed the concept of a channel and all packet flow is between chains rather than being isolated module-module communication. Thus, an application on a sending chain is allowed to send a packet to ANY other application on a destination chain by identifying the application with the portIDs in the packet. Thus, it is now the responsibility of applications to restrict which applications are allowed to send packets to them by checking the portID in the callback and rejecting any packet that comes from an unauthorized application. + +### Motivation + +The interblockchain communication protocol is designed to facilitate module-to-module communication, where modules are independent, possibly mutually distrusted, self-contained +elements of code executing on sovereign ledgers. + +## Technical Specification + +### Registering a port + +The IBC handler MUST provide a way for applications to register their callbacks on a portID. + +```typescript +function registerPort(portId: Identifier, cbs: ICS26App) => void +``` + +RegisterPort Preconditions: +- There is no other application that is registered on the port router for the given `portId`. + +RegisterPort Postconditions: +- The ICS26 application is registered on the provided `portId`. +- Any incoming packet flow message addressed to the `portId` is routed to the ICS26 application. Any outgoing packet flow message addressed by the `portId` MUST come from the ICS26 application + +### Authenticating and Routing Packet Flow Messages + +Once an application is registered with a port, it is the port router's responsibility to properly route packet flow messages to the appropriate application identified by the portId in the payload. Similarly when the application sends packet flow messages to the port router, the router MUST ensure that the application is authenticated to send the packet flow message by checking if the payload portIDs are registered to the application. + +For packet flow messages on the packet sending chain (e.g. `SendPacket`, `AcknowledgePacket`, `TimeoutPacket`); the port router MUST do this authentication and routing using the packet payload's `sourcePortId`. + +For packet flow messages on the packet receiving chain (e.g. `RecvPacket` and optionally the asynchronous `WriteAcknowledgement`); the port router MUST do this authentication and routing using the packet payload's `destPortId`. + +[ICS-4](../ics-004-channel-and-packet-semantics/PACKET_HANDLER.md) defines the packet flow messages and the expected behavior of their respected handlers. When the packet flow message arrives from the core ICS-4 handler to the application (e.g. `RecvPacket`, `AcknowledgePacket`, `TimeoutPacket`); then the portRouter acts as a router routing the message from the core handler to the ICS26 application. When the packet flow message arrives from the application to the core ICS-4 handler (e.g. `SendPacket`, or the optional `WriteAcknowledgement`); then the portRouter acts as an authenticator by checking that the calling application is registered as the owner of port they wish to send the message on before sending the message to the ICS-4 handler. + +NOTE: It is possible for implementations to change the order of execution flow so long as they still respect all the expected semantics and behavior defined in ICS-4. In this case, the port router's role as router or authenticator will change accordingly. \ No newline at end of file diff --git a/spec/IBC_V2/ics-024-host-requirements/README.md b/spec/IBC_V2/ics-024-host-requirements/README.md new file mode 100644 index 000000000..dcdc3e816 --- /dev/null +++ b/spec/IBC_V2/ics-024-host-requirements/README.md @@ -0,0 +1,145 @@ +--- +ics: 24 +title: Host State Machine Requirements +stage: draft +category: IBC/TAO +kind: interface +requires: 23 +required-by: 4 +version compatibility: ibc-go v10.0.0 +author: Aditya Sripal +created: 2024-08-21 +modified: 2024-08-21 +--- + +## Synopsis + +This specification defines the minimal set of properties which must be fulfilled by a state machine hosting an implementation of the interblockchain communication protocol. IBC relies on a key-value provable store for cross-chain communication. In version 2 of the specification, the expected key-value storage will only be for the keys that are relevant for packet processing. + +### Motivation + +IBC is designed to be a common standard which will be hosted by a variety of blockchains & state machines and must clearly define the requirements of the host. + +### Definitions + +### Desired Properties + +IBC should require as simple an interface from the underlying state machine as possible to maximise the ease of correct implementation. + +## Technical Specification + +### Module system + +The host state machine must support a module system, whereby self-contained, potentially mutually distrusted packages of code can safely execute on the same ledger, control how and when they allow other modules to communicate with them, and be identified and manipulated by a "master module" or execution environment. + +The IBC core handlers as defined in ICS-4 must have + +### Paths, identifiers, separators + +An `Identifier` is a bytestring used as a key for an object stored in state, such as a packet commitment, acknowledgement, or receipt. + +Identifiers MUST be non-empty (of positive integer length). + +Identifiers MUST consist of characters in one of the following categories only: + +- Alphanumeric +- `.`, `_`, `+`, `-`, `#` +- `[`, `]`, `<`, `>` + +A `Path` is a bytestring used as the key for an object stored in state. Paths MUST contain only identifiers, constant bytestrings, and the separator `"/"`. + +Identifiers are not intended to be valuable resources — to prevent name squatting, minimum length requirements or pseudorandom generation MAY be implemented, but particular restrictions are not imposed by this specification. + +The separator `"/"` is used to separate and concatenate two identifiers or an identifier and a constant bytestring. Identifiers MUST NOT contain the `"/"` character, which prevents ambiguity. + +By default, identifiers have the following minimum and maximum lengths in characters: + +| Port identifier | Client identifier | +| --------------- | ----------------- | +| 2 - 128 | 2 - 64 | + +### Key/value Store + +The host state machine MUST provide a key/value store interface +with three functions that behave in the standard way: + +```typescript +type get = (path: Path) => Value | void +``` + +```typescript +type set = (path: Path, value: Value) => void +``` + +```typescript +type queryProof = (path: Path) => (CommitmentProof, Value) +``` + +`queryProof` will return a `Membership` proof if there exists a value for that path in the key/value store and a `NonMembership` proof if there is no value stored for the path. + +The host state machine SHOULD provide an interface for deleting +a Path from the key/value store as well though it is not required: + +```typescript +type delete = (path: Path) => void +``` + +`Path` is as defined above. `Value` is an arbitrary bytestring encoding of a particular data structure. The specific Path and Values required to be written to the provable store are defined in [ICS-4](../ics-004-channel-and-packet-semantics/PACKET.md). + +These functions MUST be permissioned to the IBC handler module (the implementation of which is described in [ICS-4](../ics-004-channel-and-packet-semantics/PACKET_HANDLER.md)) only, so only the IBC handler module can `set` or `delete` the paths that can be read by `get`. + +In most cases, this will be implemented as a sub-store (prefixed key-space) of a larger key/value store used by the entire state machine. This is why ICS-2 defines a `counterpartyCommitmentPrefix` that is associated with the client. The IBC handler will prefix the `counterpartyCommitmentPrefix` to the ICS-4 standardized path before proof verification against a `ConsensusState` in the client. + +### Provable Path-space + +IBC/TAO implementations MUST implement the following paths for the `provableStore` in the exact format specified. This is because counterparty IBC/TAO implementations will construct the paths according to this specification and send it to the light client to verify the IBC specified value stored under the IBC specified path. + +Future paths may be used in future versions of the protocol, so the entire key-space in the provable store MUST be reserved for the IBC handler. + +| Value | Path format | Value type | Defined in | +| -------------------------- | ------------------------------------------------- | ---------- | ------------------------------------ | +| Packet Commitment | {channelIdentifier}|0x1|{bigEndianUint64Sequence} | bytes | [ICS 4](../ics-004-channel-and-packet-semantics) | +| Packet Receipt | {channelIdentifier}|0x2|{bigEndianUint64Sequence} | bytes | [ICS 4](../ics-004-channel-and-packet-semantics) | +| Acknowledgement Commitment | {channelIdentifier}|0x3|{bigEndianUint64Sequence} | bytes | [ICS 4](../ics-004-channel-and-packet-semantics) | + +IBC V2 only proves commitments related to packet handling, thus the commitments and how to construct them are specifed in [ICS-4](../ics-004-channel-and-packet-semantics/PACKET.md). + +As mentioned above, the provable path space controlled by the IBC handler may be prefixed in a global provable key/value store. In this case, the prefix must be appended by the IBC handler before the proof is verified. + +The provable store MUST be capable of providing `MembershipProof` for a key/value pair that exists in the store. It MUST also be capable of providing a `NonMembership` proof for a key that does not exist in the store. + +In the case, the state machine does not support `NonMembership` proofs; a client may get around this restriction by associating a `SENTINEL_ABSENCE_VALUE` with meaning the key does not exist and treating a `MembershipProof` with a `SENTINEL_ABSENCE_VALUE` as a `NonMembershipProof`. In this case, the state machine is responsible for ensuring that there is a way to write a `SENTINEL_ABSENCE_VALUE` to the keys that IBC needs to prove nonmembership for and it MUST ensure that an actor cannot set the `SENTINEL_ABSENCE_VALUE` directly for a key accidentally. These requirements and how to implement them are outside the scope of this specification and remain the responsibility of the bespoke IBC implementation. + +### Finality + +The state machine MUST make updates sequentially so that all state updates happen in order and can be associated with a unique `Height` in that order. Each state update at a height `h` MUST be eventually **finalized** at a finite timestamp `t` such that the order of state updates from the initial state up to `h` will never change after time `t`. + +IBC handlers will only accept packet-flow messages from state updates which are already deemed to be finalized. In cases where the finality property is probabilistically guaranteed, this probabilitic guarantee must be handled within the ICS-2 client in order to provide a final view of the remote state machine for the ICS-4 packet handler. + +### Time + +As the state updates are applied to the state machine over time, the state update algorithm MUST itself have secure access to the current timestamp at which the state update is being applied. This is needed for IBC handlers to process timeouts correctly. + +If the state machine update mechanism does not itself provide a timestamp to the state machine handler, then there must be a time oracle updates as part of the state machine update itself. In this case, the security model of IBC will also include the security model of the time oracle. + +This timestamp for a state update MUST be monotonically increasing and it MUST be the greater than or equal to the timestamp that the counterparty client will return for the `ConsensusState` associated with that state update. + +## Backwards Compatibility + +Not applicable. + +## Forwards Compatibility + +Key/value store functionality and consensus state type are unlikely to change during operation of a single host state machine. + +`submitDatagram` can change over time as relayers should be able to update their processes. + +## Example Implementations + +## History + +Aug 21, 2024 - [Initial draft](https://github.com/cosmos/ibc/pull/1144) + +## Copyright + +All content herein is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/spec/IBC_V2/ics-026-application-callbacks/README.md b/spec/IBC_V2/ics-026-application-callbacks/README.md new file mode 100644 index 000000000..dc6c5f395 --- /dev/null +++ b/spec/IBC_V2/ics-026-application-callbacks/README.md @@ -0,0 +1,98 @@ +--- +ics: 26 +title: IBC Application Callbacks +stage: Draft +category: IBC/TAO +kind: instantiation +version compatibility: ibc-go v10.0.0 +author: Aditya Sripal +created: 2025-03-19 +--- + +## Synopsis + +IBC enables module to module communication across remote state machines by providing a secure packet flow authenticated by the ICS-4 packet handler. The IBC core protocol is responsible for TAO (transport, authentication, ordering) of packets between two chains. These packets contain payload(s) that carry the application-specific information that is being communicated between two ICS26 applications. The data in the payload is itself opaque to the IBC core protocol, IBC core only verifies that it was correctly sent by the sender and then provides that data to the receiver for application-specific interpretation and processing. + +This specification standardizes the interface between ICS-4 (core IBC/TAO) and an IBC application (i.e. ICS26 app) for all the packet flow messages. + +The default IBC handler uses a receiver call pattern, where modules must individually call the IBC handler in order to send packets. In turn, the IBC handler verifies incoming packet flow messages like `ReceivePacket`, `AcknowledgePacket` and `TimeoutPacket` and calls into the appropriate ICS26 application as described in [ICS5 Port Allocation](../ics-005-port-allocation/README.md). + +## Technical Specification + +### Payload Structure + +The payload structure is reproduced from [ICS-4](../ics-004-channel-and-packet-semantics/PACKET.md) since all of the following application functions are operating on the payloads that are being sent in the packets. + +```typescript +interface Payload { + sourcePort: bytes, // identifier of the sending application on the sending chain + destPort: bytes, // identifier of the receiving application on the receiving chain + version: string, // payload version only interpretable by sending/receiving applications + encoding: string, // payload encoding only interpretable by sending/receiving applications + value: bytes // application-specific data that can be parsed by receiving application given the version and encoding +} +``` + +### Core Handler Interface Exposed to ICS26 Applications + +The IBC core handler MUST expose the following function signature to the ICS26 applications registed on the port router, so that the application can send packets. + +### SendPacket + +SendPacket Inputs: +`payload: Payload`: This is the payload that the application wishes to send to an application on the receiver chain. +`sourceClientId: bytes`: Identifier of the receiver chain client that exists on the sending chain. +`timeoutTimestamp: uint64`: The timeout in UNIX seconds after which the packet is no longer receivable on the receiving chain. NOTE: This timestamp is evaluated against the **receiving chain** clock as there may be drift between the sending chain and receiving chain clocks + +SendPacket Preconditions: +- The application is registered on the port router with `payload.SourcePortId` +- The application MUST have successfully conducted any application specific logic necessary for sending the given payload. +- The sending client exists for `sourceClientId` + +SendPacket Postconditions: +- The following packet gets committed and stored under the packet commitment path as specified by ICS24: +```typescript +interface Packet { + sourceClientId: msg.sourceClientId, + destClientId: counterparty.ClientId, + sequence: generateUniqueSequence(sourceClientId), + timeoutTimestamp: msg.timeoutTimestamp + data: msg.Payloads +} +``` +- The sequence is returned to the ICS26 application + +SendPacket ErrorConditions: +- The sending client is invalid (expired or frozen) +- The provided `timeoutTimstamp` has already elapsed + +NOTE: IBC v2 allows multiple payloads coming from multiple applications to be sent in the same packet. If an implementation chooses to support this feature, they may either provide an entrypoint in the core handler to send multiple packets, which must then call each individual application `OnSendPacket` callback to validate their individual payload and do application-specific sending logic; or they may queue the payloads coming from each application until the packet is ready to be committed. + +### WriteAcknowledgement + +The IBC core handler MAY expose the following function signature to the ICS26 applications registed on the port router, so that the application can write acknowledgements asynchronously. + +This is only necessary if the implementation supports processing packets asynchronously. In this case, an application may process the packet asynchronously from when the IBC core handler receives the packet. Thus, the acknowledgement cannot be returned as part of the `OnRecvPacket` callback and must be submitted to the core IBC handler by the ICS26 application at a later time. Thus, we must introduce a new endpoint on the IBC handler for the ICS26 application to call when it is done processing a receive packet and wants to write the acknowledgement. + +WriteAcknowledgement Inputs: + +WriteAcknowledgement Inputs: +`destClientId: bytes`: Identifier of the sender chain client that exist on the receiving chain +`sequence: uint64`: Unique sequence identifying the packet from sending chain to receiving chain +`ack: bytes`: Acknowledgement from the receiving application for the payload it was sent by the application. If the receive was unsuccessful, the `ack` must be the `SENTINEL_ERROR_ACKNOWLEDGEMENT`, otherwise it may be some application-specific data. + +WriteAcknowledgement Preconditions: +- A packet receipt is stored under the specified ICS24 with the `destClientId` and `sequence` +- An acknowledgement for the `destClientId` and `sequence` has not already been written under the ICS24 path + +WriteAcknowledgement Postconditions: +- The acknowledgement is committed and written to the acknowledgement path as specified in ICS24 +- If the acknowledgement is successful, then all receiving applications must have executed their recvPacket logic and written state +- If the acknowledgement is unsuccessful (ie ERROR ACK), any state changes made by the receiving applications MUST all be reverted. This ensure atomic execution of the multi-payload packet. + +NOTE: In the case that the packet contained multiple payloads, the IBC core handler MUST wait for all applications to return their individual acknowledgements for the packet before commiting the acknowledgment. If ANY application returns the error acknowledgement, then the acknowledgement for the entire packet only contains the `ERROR_SENTINEL_ACKNOWLEDGEMENT`. Otherwise, the acknowledgment is a list containing each applications individual acknowledgment in the same order that their associated payload existed in the packet. + + +### ICS26 Interface Exposed to Core Handler + +Modules must expose the following function signatures to the routing module, which are called upon the receipt of various datagrams: diff --git a/spec/app/ics-020-fungible-token-transfer/README.md b/spec/app/ics-020-fungible-token-transfer/README.md index c5eaa5523..fb5dc8064 100644 --- a/spec/app/ics-020-fungible-token-transfer/README.md +++ b/spec/app/ics-020-fungible-token-transfer/README.md @@ -9,7 +9,7 @@ kind: instantiation version compatibility: author: Christopher Goes , Aditya Sripal created: 2019-07-15 -modified: 2024-03-05 +modified: 2024-10-31 --- ## Synopsis @@ -70,6 +70,7 @@ interface Denom { interface Forwarding { hops: []Hop + timeoutTimestamp: uint64 memo: string } @@ -205,11 +206,11 @@ interface ModuleState { #### Packet forward path -The `v2` packets that have non-empty forwarding information and should thus be forwarded, must be stored in the private store, so that an acknowledgement can be written for them when receiving an acknowledgement or timeout for the forwarded packet. +For the `v2` packets that have non-empty forwarding information and should thus be forwarded, the `sequence` , `destChannelId` and `destPort` must be stored in the private store, so that an acknowledgement can be written for them when receiving an acknowledgement or timeout for the forwarded packet. ```typescript -function packetForwardPath(portIdentifier: Identifier, channelIdentifier: Identifier, sequence: uint64): Path { - return "forwardedPackets/ports/{portIdentifier}/channels/{channelIdentifier}/sequences/{sequence}" +function packetForwardPath(channelIdentifier: bytes, sequence: bigEndianUint64): Path { + return "{channelIdentifier}0x4{bigEndianUint64Sequence}" } ``` @@ -217,128 +218,42 @@ function packetForwardPath(portIdentifier: Identifier, channelIdentifier: Identi The sub-protocols described herein should be implemented in a "fungible token transfer bridge" module with access to a bank module and to the IBC routing module. -#### Port & channel setup +#### Application callback setup -The `setup` function must be called exactly once when the module is created (perhaps when the blockchain itself is initialised) to bind to the appropriate port and create an escrow address (owned by the module). +The `setup` function must be called exactly once when the module is created (perhaps when the blockchain itself is initialised) to register the application callbacks in the IBC router. ```typescript function setup() { - capability = routingModule.bindPort("transfer", ModuleCallbacks{ - onChanOpenInit, - onChanOpenTry, - onChanOpenAck, - onChanOpenConfirm, - onChanCloseInit, - onChanCloseConfirm, - onRecvPacket, - onTimeoutPacket, - onAcknowledgePacket, - onTimeoutPacketClose - }) - claimCapability("port", capability) + IBCRouter.callbacks["transfer"]=[onSendPacket,onRecvPacket,onAcknowledgePacket,onTimeoutPacket] } ``` -Once the `setup` function has been called, channels can be created through the IBC routing module between instances of the fungible token transfer module on separate chains. - -An administrator (with the permissions to create connections & channels on the host state machine) is responsible for setting up connections to other state machines & creating channels -to other instances of this module (or another module supporting this interface) on other chains. This specification defines packet handling semantics only, and defines them in such a fashion -that the module itself doesn't need to worry about what connections or channels might or might not exist at any point in time. +Once the `setup` function has been called, the application callbacks are registered and accessible in the IBC router. #### Routing module callbacks -##### Channel lifecycle management - -Both machines `A` and `B` accept new channels from any module on another machine, if and only if: - -- The channel being created is unordered. -- The version string is `ics20-1` or `ics20-2`. +##### Utility functions ```typescript -function onChanOpenInit( - order: ChannelOrder, - connectionHops: [Identifier], - portIdentifier: Identifier, - channelIdentifier: Identifier, - counterpartyPortIdentifier: Identifier, - counterpartyChannelIdentifier: Identifier, - version: string) => (version: string, err: Error) { - // only unordered channels allowed - abortTransactionUnless(order === UNORDERED) - // assert that version is "ics20-1" or "ics20-2" or empty - // if empty, we return the default transfer version to core IBC - // as the version for this channel - abortTransactionUnless(version === "ics20-2" || version === "ics20-1" || version === "") - // allocate an escrow address - channelEscrowAddresses[channelIdentifier] = newAddress(portIdentifier, channelIdentifier) - if version == "" { - // default to latest supported version - return "ics20-2", nil +function unmarshal(encoding: Encoding, version: string, appData: bytes): bytes{ + if (version == "ics20-v1"){ + FungibleTokenPacketData data = decode(encoding,appData) + } + if (version == "ics20-v2"){ + FungibleTokenPacketDataV2 data = decode(encoding,appData) } - // If the version is not empty and is among those supported, we return the version - return version, nil -} -``` - -```typescript -function onChanOpenTry( - order: ChannelOrder, - connectionHops: [Identifier], - portIdentifier: Identifier, - channelIdentifier: Identifier, - counterpartyPortIdentifier: Identifier, - counterpartyChannelIdentifier: Identifier, - counterpartyVersion: string) => (version: string, err: Error) { - // only unordered channels allowed - abortTransactionUnless(order === UNORDERED) - // assert that version is "ics20-1" or "ics20-2" - abortTransactionUnless(counterpartyVersion === "ics20-1" || counterpartyVersion === "ics20-2") - // allocate an escrow address - channelEscrowAddresses[channelIdentifier] = newAddress(portIdentifier, channelIdentifier) - // return the same version as counterparty version so long as we support it - return counterpartyVersion, nil -} -``` - -```typescript -function onChanOpenAck( - portIdentifier: Identifier, - channelIdentifier: Identifier, - counterpartyChannelIdentifier: Identifier, - counterpartyVersion: string) { - // port has already been validated - // assert that counterparty selected version is the same as our version - channel = provableStore.get(channelPath(portIdentifier, channelIdentifier)) - abortTransactionUnless(counterpartyVersion === channel.version) -} -``` - -```typescript -function onChanOpenConfirm( - portIdentifier: Identifier, - channelIdentifier: Identifier) { - // accept channel confirmations, port has already been validated, version has already been validated + if (version != "ics20-v1" && version!= "ics20-v2"){ + return nil + } + return data; } ``` -```typescript -function onChanCloseInit( - portIdentifier: Identifier, - channelIdentifier: Identifier) { - // always abort transaction - abortTransactionUnless(FALSE) -} -``` +##### Packet relay -```typescript -function onChanCloseConfirm( - portIdentifier: Identifier, - channelIdentifier: Identifier) { - // no action necessary -} -``` +This specification defines packet handling semantics. -##### Packet relay +Both machines `A` and `B` accept new packet from any module on another machine, if and only if the version string is `ics20-1` or `ics20-2`. In plain English, between chains `A` and `B`: @@ -351,75 +266,48 @@ In plain English, between chains `A` and `B`: Note: `constructOnChainDenom` is a helper function that will construct the local on-chain denomination for the bridged token. It **must** encode the trace and base denomination to ensure that tokens coming over different paths are not treated as fungible. The original trace and denomination must be retrievable by the state machine so that they can be passed in their original forms when constructing a new IBC path for the bridged token. The ibc-go implementation handles this by creating a local denomination: `hash(trace+base_denom)`. -`sendFungibleTokens` must be called by a transaction handler in the module which performs appropriate signature checks, specific to the account owner on the host state machine. +`onSendFungibleTokens` must be called by a transaction handler in the module which performs appropriate signature checks, specific to the account owner on the host state machine. ```typescript -function sendFungibleTokens( - tokens: []Token, - sender: string, - receiver: string, - memo: string, - forwarding: Forwarding, - sourcePort: string, - sourceChannel: string, - timeoutHeight: Height, - timeoutTimestamp: uint64, // in unix nanoseconds -): uint64 { +function onSendFungibleTokens( + sourceChannelId: bytes, + payload: Payload + ): bool { + + // the unmarshal function must check the payload.encoding is among those supported + appData=unmarshal(payload.encoding, payload.version, payload.appData) + abortTransactionUnless(appData!=nil) + + transferVersion = payload.version + if transferVersion == "ics20-1" { + abortTransactionUnless(len(appData.tokens) == 1) + // abort if forwarding defined + abortTransactionUnless(appData.forwarding == nil) + } else if transferVersion == "ics20-2" { + // No-Op + } else { + // Unsupported transfer version + abortTransactionUnless(false) + } + // memo and forwarding cannot both be non-empty - abortTransactionUnless(memo != "" && forwarding != nil) - for token in tokens + abortTransactionUnless(appData.memo != "" && appData.forwarding != nil) + for token in appData.tokens onChainDenom = constructOnChainDenom(token.denom.trace, token.denom.base) // if the token is not prefixed by our channel end's port and channel identifiers // then we are sending as a source zone - if !isTracePrefixed(sourcePort, sourceChannel, token) { + if !isTracePrefixed(payload.sourcePort, sourceChannelId, token) { // determine escrow account - escrowAccount = channelEscrowAddresses[sourceChannel] + escrowAccount = channelEscrowAddresses[sourceChannelId] // escrow source tokens (assumed to fail if balance insufficient) - bank.TransferCoins(sender, escrowAccount, onChainDenom, token.amount) + bank.TransferCoins(appData.sender, escrowAccount, onChainDenom, token.amount) } else { // receiver is source chain, burn vouchers - bank.BurnCoins(sender, onChainDenom, token.amount) + bank.BurnCoins(appData.sender, onChainDenom, token.amount) } } - var dataBytes bytes - channel = provableStore.get(channelPath(sourcePort, sourceChannel)) - // getAppVersion returns the transfer version that is embedded in the channel version - // as the channel version may contain additional app or middleware version(s) - transferVersion = getAppVersion(channel.version) - if transferVersion == "ics20-1" { - abortTransactionUnless(len(tokens) == 1) - token = tokens[0] - // abort if forwarding defined - abortTransactionUnless(forwarding == nil) - // create v1 denom of the form: port1/channel1/port2/channel2/port3/channel3/denom - v1Denom = constructOnChainDenom(token.denom.trace, token.denom.base) - // v1 packet data does not support forwarding fields - data = FungibleTokenPacketData{v1Denom, token.amount, sender, receiver, memo} - // JSON-marshal packet data into bytes - dataBytes = json.marshal(data) - } else if transferVersion == "ics20-2" { - // create FungibleTokenPacket data - data = FungibleTokenPacketDataV2{tokens, sender, receiver, memo, forwarding} - // protobuf-marshal packet data into bytes - dataBytes = protobuf.marshal(data) - } else { - // should never be reached as transfer version must be negotiated to be either - // ics20-1 or ics20-2 during channel handshake - abortTransactionUnless(false) - } - - // send packet using the interface defined in ICS4 - sequence = handler.sendPacket( - getCapability("port"), - sourcePort, - sourceChannel, - timeoutHeight, - timeoutTimestamp, - dataBytes, - ) - - return sequence + return true } ``` @@ -428,49 +316,51 @@ function sendFungibleTokens( Note: Function `parseICS20V1Denom` is a helper function that will take the full IBC denomination and extract the base denomination (i.e. native denomination in the chain of origin) and the trace information (if any) for the received token. ```typescript -function onRecvPacket(packet: Packet) { - channel = provableStore.get(channelPath(portIdentifier, channelIdentifier)) - // getAppVersion returns the transfer version that is embedded in the channel version - // as the channel version may contain additional app or middleware version(s) - transferVersion = getAppVersion(channel.version) +function onRecvPacket( + destChannelId: bytes, + sourceChannelId: bytes, + sequence: uint64, + payload: Payload, + relayer: address + ): (bytes, bool) { + transferVersion = payload.version var tokens []Token var sender string var receiver string // address to send tokens to on this chain var finalReceiver string // final intended address in forwarding case if transferVersion == "ics20-1" { - FungibleTokenPacketData data = json.unmarshal(packet.data) + data = unmarshal(payload.encoding, payload.version, payload.appData) // convert full denom string to denom struct with base denom and trace - denom = parseICS20V1Denom(data.denom) - token = Token{ - denom: denom - amount: data.amount - } - tokens = []Token{token} - sender = data.sender - receiver = data.receiver - } else if transferVersion == "ics20-2" { - FungibleTokenPacketDataV2 data = protobuf.unmarshal(packet.data) - tokens = data.tokens + denom = parseICS20V1Denom(data.denom) + token = Token{ + denom: denom + amount: data.amount + } + tokens = []Token{token} sender = data.sender - - // if we need to forward the tokens onward - // overwrite the receiver to temporarily send to the - // channel escrow address of the intended receiver - if len(data.forwarding.hops) > 0 { - // memo must be empty - abortTransactionUnless(data.memo == "") - if channelForwardingAddress[packet.destChannel] == "" { - channelForwardingAddress[packet.destChannel] = newAddress() + receiver = data.receiver + } else if transferVersion == "ics20-2" { + data = unmarshal(payload.encoding, payload.version, payload.appData) + tokens = data.tokens + sender = data.sender + + // if we need to forward the tokens onward + // overwrite the receiver to temporarily send to the + // channel escrow address of the intended receiver + if len(data.forwarding.hops) > 0 { + // memo must be empty + abortTransactionUnless(data.memo == "") + if channelForwardingAddress[destChannelId] == "" { + channelForwardingAddress[destChannelId] = newAddress() } - receiver = channelForwardingAddresses[packet.destChannel] + receiver = channelForwardingAddresses[destChannelId] finalReceiver = data.receiver } else { receiver = data.receiver } } else { - // should never be reached as transfer version must be negotiated - // to be either ics20-1 or ics20-2 during channel handshake + // should never be reached as transfer version must be either ics20-1 or ics20-2 during channel handshake abortTransactionUnless(false) } @@ -491,13 +381,13 @@ function onRecvPacket(packet: Packet) { // port and channel identifiers then we are receiving tokens we // previously had sent to the sender, thus we are receiving the tokens // as a source zone - if isTracePrefixed(packet.sourcePort, packet.sourceChannel, token) { + if isTracePrefixed(payload.sourcePort, sourceChannelId, token) { // since we are receiving back to source we remove the prefix from the trace onChainTrace = token.trace[1:] onChainDenom = constructOnChainDenom(onChainTrace, token.denom.base) // receiver is source chain: unescrow tokens // determine escrow account - escrowAccount = channelEscrowAddresses[packet.destChannel] + escrowAccount = channelEscrowAddresses[destChannelId] // unescrow tokens to receiver (assumed to fail if balance insufficient) err = bank.TransferCoins(escrowAccount, receiver, onChainDenom, token.amount) if (err != nil) { @@ -507,7 +397,7 @@ function onRecvPacket(packet: Packet) { } } else { // since we are receiving to a new sink zone we prepend the prefix to the trace - prefixTrace = Hop{portId: packet.destPort, channelId: packet.destChannel} + prefixTrace = Hop{portId: payload.destPort, channelId: destChannelId} onChainTrace = append([]Hop{prefixTrace}, token.denom.trace...) onChainDenom = constructOnChainDenom(onChainTrace, token.denom.base) // sender was source, mint vouchers to receiver (assumed to fail if balance insufficient) @@ -529,50 +419,56 @@ function onRecvPacket(packet: Packet) { // if there is an error ack return immediately and do not forward further if !ack.Success() { - return ack + return ack, true } // if acknowledgement is successful and forwarding path set // then start forwarding - if len(forwarding.hops) > 0 { - //check that next channel supports token forwarding - channel = provableStore.get(channelPath(forwarding.hops[0].portId, forwarding.hops[0].channelId)) - if channel.version != "ics20-2" && len(forwarding.hops) > 1 { - ack = FungibleTokenPacketAcknowledgement(false, "next hop in path cannot support forwarding onward") - return ack - } + if len(data.forwarding.hops) > 0 { + memo = "" + originalTimeoutTimestamp = data.forwarding.timeoutTimestamp nextForwarding = Forwarding{ - hops: forwarding.hops[1:] - memo: forwarding.memo + hops: data.forwarding.hops[1:] + timeoutTimestamp: originalTimeoutTimestamp // pass the original timestamp value + memo: data.forwarding.memo } - if len(forwarding.hops) == 1 { + if len(data.forwarding.hops) == 1 { // we're on the last hop, we can set memo and clear // the next forwarding - memo = forwarding.memo + memo = data.forwarding.memo nextForwarding = nil } // send the tokens we received above to the next port and channel // on the forwarding path // and reduce the forwarding by the first element - packetSequence = sendFungibleTokens( - receivedTokens, - receiver, // sender of next packet - finalReceiver, // receiver of next packet - memo, - nextForwarding, - forwarding.hops[0].portId, + + // Here we must call the core sendPacket providing the correct forwardingPayload --> Need to construct the payload + //construct payload + + forwardingPayload= FungibleTokenPacketDataV2 { + tokens: receivedTokens, + sender: receiver + receiver: finalReceiver + memo: memo, + // a struct containing the list of next hops, + // determining where the tokens must be forwarded next, + // and the memo for the final hop + forwarding: nextForwarding + } + + packetSequence=handler.sendPacket( forwarding.hops[0].channelId, - Height{}, - currentTime() + DefaultHopTimeoutPeriod, + originalTimeoutTimestamp, + forwardingPayload ) - // store packet for future sending ack - privateStore.set(packetForwardPath(forwarding.hops[0].portId, forwarding.hops[0].channelId, packetSequence), packet) + // store previous packet sequence and destChannelId for future sending ack + privateStore.set(packetForwardPath(forwarding.hops[0].channelId, packetSequence), sequence, destChannelId, payload.destPort) // use async ack until we get successful acknowledgement from further down the line. - return nil + return nil, true } - return ack + return ack,true } ``` @@ -580,72 +476,88 @@ function onRecvPacket(packet: Packet) { ```typescript function onAcknowledgePacket( - packet: Packet, - acknowledgement: bytes) { + sourceChannelId: bytes, + destChannelId: bytes, + sequence: uint64, + payload: Payload, + acknowledgement: bytes, + relayer: address + ): bool { // if the transfer failed, refund the tokens // to the sender account. In case of a packet sent for a // forwarded packet, the sender is the forwarding // address for the destination channel of the forwarded packet. if !(acknowledgement.success) { - refundTokens(packet) + refundTokens(sourceChannelId, payload) } // check if the packet that was sent is from a previously forwarded packet - prevPacket = privateStore.get(packetForwardPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) + prevPacketSeq,prevPacketDestChannelId, prevPacketDestPort = privateStore.get(packetForwardPath(sourceChannelId, sequence)) - if prevPacket != nil { + if prevPacketSeq != 0 { if acknowledgement.success { FungibleTokenPacketAcknowledgement ack = FungibleTokenPacketAcknowledgement{true, "forwarded packet succeeded"} handler.writeAcknowledgement( - prevPacket, + prevPacketDestChannelId, + prevPacketSeq, ack, ) } else { // the forwarded packet has failed, thus the funds have been refunded to the forwarding address. // we must revert the changes that came from successfully receiving the tokens on our chain // before propogating the error acknowledgement back to original sender chain - revertInFlightChanges(packet, prevPacket) + revertInFlightChanges(destChannelId,payload,prevPacketDestChannelId,prevPacketDestPort) // write error acknowledgement FungibleTokenPacketAcknowledgement ack = FungibleTokenPacketAcknowledgement{false, "forwarded packet failed"} handler.writeAcknowledgement( - prevPacket, + prevPacketDestChannelId, + prevPacketSeq, ack, ) } - // delete the forwarded packet that triggered sending this packet - privateStore.delete(packetForwardPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) + // delete the forwarded packet info that triggered sending this packet + privateStore.delete(packetForwardPath(sourceChannelId, sequence)) } + + return true } ``` `onTimeoutPacket` is called by the routing module when a packet sent by this module has timed-out (such that it will not be received on the destination chain). ```typescript -function onTimeoutPacket(packet: Packet) { +function onTimeoutPacket( + sourceChannelId: bytes, + destChannelId: bytes, + sequence: uint64, + payload: Payload, + relayer: address + ): bool { // the packet timed-out, so refund the tokens // to the sender account. In case of a packet sent for a // forwarded packet, the sender is the forwarding // address for the destination channel of the forwarded packet. - refundTokens(packet) + refundTokens(sourceChannelId,payload) // check if the packet sent is from a previously forwarded packet - prevPacket = privateStore.get(packetForwardPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) + prevPacketSeq,prevPacketDestChannelId, prevPacketDestPort = privateStore.get(packetForwardPath(sourceChannelId, sequence)) - if prevPacket != nil { + if prevPacketSeq != nil { // the forwarded packet has failed, thus the funds have been refunded to the forwarding address. // we must revert the changes that came from successfully receiving the tokens on our chain // before propogating the error acknowledgement back to original sender chain - revertInFlightChanges(packet, prevPacket) + revertInFlightChanges(destChannelId, payload, prevPacketDestChannelId,prevPacketDestPort) // write error acknowledgement FungibleTokenPacketAcknowledgement ack = FungibleTokenPacketAcknowledgement{false, "forwarded packet timed out"} handler.writeAcknowledgement( - prevPacket, + prevPacketDestChannelId + prevPacketSeq, ack, ) // delete the forwarded packet that triggered sending this packet - privateStore.delete(packetForwardPath(packet.sourcePort, packet.sourceChannel, packet.sequence)) + privateStore.delete(packetForwardPath(sourceChannelId, sequence)) } } ``` @@ -670,13 +582,14 @@ function isTracePrefixed(portId: string, channelId: string, token: Token) boolea `refundTokens` is called by both `onAcknowledgePacket`, on failure, and `onTimeoutPacket`, to refund escrowed tokens to the original sender. ```typescript -function refundTokens(packet: Packet) { - channel = provableStore.get(channelPath(portIdentifier, channelIdentifier)) - // getAppVersion returns the transfer version that is embedded in the channel version - // as the channel version may contain additional app or middleware version(s) - transferVersion = getAppVersion(channel.version) +function refundTokens( + sourceChannelId: bytes, + payload: Payload + ) { + // retrieve version from payload + transferVersion = payload.version if transferVersion == "ics20-1" { - FungibleTokenPacketData data = json.unmarshal(packet.data) + data = unmarshal(payload.encoding,payload.version,payload.appData) // convert full denom string to denom struct with base denom and trace denom = parseICS20V1Denom(data.denom) token = Token{ @@ -685,11 +598,10 @@ function refundTokens(packet: Packet) { } tokens = []Token{token} } else if transferVersion == "ics20-2" { - FungibleTokenPacketDataV2 data = protobuf.unmarshal(packet.data) - tokens = data.tokens + data = unmarshal(payload.encoding,payload.version,payload.appData) + tokens = data.tokens } else { - // should never be reached as transfer version must be negotiated to be either - // ics20-1 or ics20-2 during channel handshake + // Unsupported version abortTransactionUnless(false) } @@ -698,9 +610,9 @@ function refundTokens(packet: Packet) { // Since this is refunding an outgoing packet, we can check if the tokens // were originally from the receiver by checking if the tokens were prefixed // by our channel end's identifiers. - if !isTracePrefixed(packet.sourcePort, packet.sourceChannel, token) { + if !isTracePrefixed(payload.sourcePortId, sourceChannelId, token) { // sender was source chain, unescrow tokens back to sender - escrowAccount = channelEscrowAddresses[packet.sourceChannel] + escrowAccount = channelEscrowAddresses[sourceChannelId] bank.TransferCoins(escrowAccount, data.sender, onChainDenom, token.amount) } else { // receiver was source chain, mint vouchers back to sender @@ -716,19 +628,26 @@ function refundTokens(packet: Packet) { // If an error occurs further down the line, the state changes // on this chain must be reverted before sending back the error acknowledgement // to ensure atomic packet forwarding -function revertInFlightChanges(sentPacket: Packet, receivedPacket: Packet) { - forwardingAddress = channelForwardingAddress[receivedPacket.destChannel] - reverseEscrow = channelEscrowAddresses[receivedPacket.destChannel] +function revertInFlightChanges( + sentPacketDestChannelId: bytes, + sentPacketPayload: Payload, + receivedPacketDestChannelId: bytes, + receivedPacketDestPort: bytes, + ) { + forwardingAddress = channelForwardingAddress[receivedPacketDestChannelId] + reverseEscrow = channelEscrowAddresses[receivedPacketDestChannelId] + + data=unmarshal(sentPacketPayload.encoding,sentPacketPayload.version,sentPacketPayload.appData) // the token on our chain is the token in the sentPacket - for token in sentPacket.tokens { + for token in data.tokens { // we are checking if the tokens that were sent out by our chain in the // sentPacket were source tokens with respect to the original receivedPacket. // If the tokens in sentPacket were prefixed by our channel end's port and channel // identifiers, then it was a minted voucher and we need to burn it. // Otherwise, it was an original token from our chain and we must give the tokens // back to the escrow account. - if !isTracePrefixed(receivedPacket.destinationPort, receivedPacket.destinationChannel, token) { + if !isTracePrefixed(receivedPacketDestPort, receivedPacketDestChannelId, token) { // receive sent tokens from the received escrow account to the forwarding account // so we must send the tokens back from the forwarding account to the received escrow account bank.TransferCoins(forwardingAddress, reverseEscrow, token.denom, token.amount) @@ -741,12 +660,6 @@ function revertInFlightChanges(sentPacket: Packet, receivedPacket: Packet) { } ``` -```typescript -function onTimeoutPacketClose(packet: Packet) { - // can't happen, only unordered channels allowed -} -``` - #### Using the Memo Field Note: Since earlier versions of this specification did not include a `memo` field, implementations must ensure that the new packet data is still compatible with chains that expect the old packet data. A legacy implementation MUST be able to unmarshal a new packet data with an empty string memo into the legacy `FungibleTokenPacketData` struct. Similarly, an implementation supporting `memo` must be able to unmarshal a legacy packet data into the current struct with the `memo` field set to the empty string. @@ -799,10 +712,7 @@ Not applicable. ## Forwards Compatibility -This initial standard uses version "ics20-1" in the channel handshake. - -A future version of this standard could use a different version in the channel handshake, -and safely alter the packet data format & packet handler semantics. +Not applicable. ## Example Implementations @@ -831,6 +741,8 @@ March 5, 2024 - [Support for path forwarding](https://github.com/cosmos/ibc/pull June 18, 2024 - [Support for data protobuf encoding](https://github.com/cosmos/ibc/pull/1118) +Oct 31, 2024 - [Support for IBC TAO v2](https://github.com/cosmos/ibc/pull/1157) + ## Copyright All content herein is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/spec/app/ics-020-fungible-token-transfer/v1/README.md b/spec/app/ics-020-fungible-token-transfer/v1/README.md index 807df20f6..67f9bfd39 100644 --- a/spec/app/ics-020-fungible-token-transfer/v1/README.md +++ b/spec/app/ics-020-fungible-token-transfer/v1/README.md @@ -9,7 +9,7 @@ kind: instantiation version compatibility: ibc-go v7.0.0, ibc-rs v0.53.0 author: Christopher Goes created: 2019-07-15 -modified: 2020-02-24 +modified: 2024-10-31 --- ## Synopsis @@ -113,29 +113,17 @@ interface ModuleState { The sub-protocols described herein should be implemented in a "fungible token transfer bridge" module with access to a bank module and to the IBC routing module. -#### Port & channel setup +#### Application callback setup -The `setup` function must be called exactly once when the module is created (perhaps when the blockchain itself is initialised) to bind to the appropriate port and create an escrow address (owned by the module). +The `setup` function must be called exactly once when the module is created (perhaps when the blockchain itself is initialised) to register the application callbacks in the IBC router. ```typescript function setup() { - capability = routingModule.bindPort("transfer", ModuleCallbacks{ - onChanOpenInit, - onChanOpenTry, - onChanOpenAck, - onChanOpenConfirm, - onChanCloseInit, - onChanCloseConfirm, - onRecvPacket, - onTimeoutPacket, - onAcknowledgePacket, - onTimeoutPacketClose - }) - claimCapability("port", capability) + IBCRouter.callbacks["transfer"]=[onSendPacket,onRecvPacket,onAcknowledgePacket,onTimeoutPacket] } ``` -Once the `setup` function has been called, channels can be created through the IBC routing module between instances of the fungible token transfer module on separate chains. +Once the `setup` function has been called, the application callbacks are registered and accessible in the IBC router. An administrator (with the permissions to create connections & channels on the host state machine) is responsible for setting up connections to other state machines & creating channels to other instances of this module (or another module supporting this interface) on other chains. This specification defines packet handling semantics only, and defines them in such a fashion @@ -143,93 +131,24 @@ that the module itself doesn't need to worry about what connections or channels #### Routing module callbacks -##### Channel lifecycle management - -Both machines `A` and `B` accept new channels from any module on another machine, if and only if: - -- The channel being created is unordered. -- The version string is `ics20-1`. +##### Utility functions ```typescript -function onChanOpenInit( - order: ChannelOrder, - connectionHops: [Identifier], - portIdentifier: Identifier, - channelIdentifier: Identifier, - counterpartyPortIdentifier: Identifier, - counterpartyChannelIdentifier: Identifier, - version: string) => (version: string, err: Error) { - // only unordered channels allowed - abortTransactionUnless(order === UNORDERED) - // assert that version is "ics20-1" or empty - // if empty, we return the default transfer version to core IBC - // as the version for this channel - abortTransactionUnless(version === "ics20-1" || version === "") - // allocate an escrow address - channelEscrowAddresses[channelIdentifier] = newAddress(portIdentifier, channelIdentifier) - return "ics20-1", nil +function unmarshal(encoding: Encoding, version: string, appData: bytes): bytes{ + if (version == "ics20-v1"){ + FungibleTokenPacketData data = decode(encoding,appData) + return data; + } else{ + return nil + } } ``` -```typescript -function onChanOpenTry( - order: ChannelOrder, - connectionHops: [Identifier], - portIdentifier: Identifier, - channelIdentifier: Identifier, - counterpartyPortIdentifier: Identifier, - counterpartyChannelIdentifier: Identifier, - counterpartyVersion: string) => (version: string, err: Error) { - // only unordered channels allowed - abortTransactionUnless(order === UNORDERED) - // assert that version is "ics20-1" - abortTransactionUnless(counterpartyVersion === "ics20-1") - // allocate an escrow address - channelEscrowAddresses[channelIdentifier] = newAddress(portIdentifier, channelIdentifier) - // return version that this chain will use given the - // counterparty version - return "ics20-1", nil -} -``` - -```typescript -function onChanOpenAck( - portIdentifier: Identifier, - channelIdentifier: Identifier, - counterpartyChannelIdentifier: Identifier, - counterpartyVersion: string) { - // port has already been validated - // assert that counterparty selected version is "ics20-1" - abortTransactionUnless(counterpartyVersion === "ics20-1") -} -``` - -```typescript -function onChanOpenConfirm( - portIdentifier: Identifier, - channelIdentifier: Identifier) { - // accept channel confirmations, port has already been validated, version has already been validated -} -``` - -```typescript -function onChanCloseInit( - portIdentifier: Identifier, - channelIdentifier: Identifier) { - // always abort transaction - abortTransactionUnless(FALSE) -} -``` +##### Packet relay -```typescript -function onChanCloseConfirm( - portIdentifier: Identifier, - channelIdentifier: Identifier) { - // no action necessary -} -``` +This specification defines packet handling semantics. -##### Packet relay +Both machines `A` and `B` accept new packet from any module on another machine, if and only if the version string is `ics20-1`. In plain English, between chains `A` and `B`: @@ -240,81 +159,75 @@ In plain English, between chains `A` and `B`: an acknowledgement of failure is preferable to aborting the transaction since it more easily enables the sending chain to take appropriate action based on the nature of the failure. -`sendFungibleTokens` must be called by a transaction handler in the module which performs appropriate signature checks, specific to the account owner on the host state machine. +`onSendFungibleTokens` must be called by a transaction handler in the module which performs appropriate signature checks, specific to the account owner on the host state machine. ```typescript -function sendFungibleTokens( - denomination: string, - amount: uint256, - sender: string, - receiver: string, - sourcePort: string, - sourceChannel: string, - timeoutHeight: Height, - timeoutTimestamp: uint64, // in unix nanoseconds -): uint64 { - prefix = "{sourcePort}/{sourceChannel}/" +function onSendFungibleTokens( + sourceChannelId:bytes, + payload: Payload + ): bool { + + appData=unmarshal(payload.encoding,payload.version,payload.appData) + abortTransactionUnless(appData!=nil) + + prefix = "{payload.sourcePort}/{sourceChannelId}/" // we are the source if the denomination is not prefixed - source = denomination.slice(0, len(prefix)) !== prefix + source = appData.denom.slice(0, len(prefix)) !== prefix if source { // determine escrow account - escrowAccount = channelEscrowAddresses[sourceChannel] + escrowAccount = channelEscrowAddresses[sourceChannelId] // escrow source tokens (assumed to fail if balance insufficient) - bank.TransferCoins(sender, escrowAccount, denomination, amount) + bank.TransferCoins(appData.sender, escrowAccount, appData.denom, appData.amount) } else { // receiver is source chain, burn vouchers - bank.BurnCoins(sender, denomination, amount) + bank.BurnCoins(appData.sender, appData.denom, appData.amount) } - // create FungibleTokenPacket data - data = FungibleTokenPacketData{denomination, amount, sender, receiver} - - // send packet using the interface defined in ICS4 - sequence = handler.sendPacket( - getCapability("port"), - sourcePort, - sourceChannel, - timeoutHeight, - timeoutTimestamp, - json.marshal(data) // json-marshalled bytes of packet data - ) - - return sequence + return true } ``` `onRecvPacket` is called by the routing module when a packet addressed to this module has been received. ```typescript -function onRecvPacket(packet: Packet) { - FungibleTokenPacketData data = packet.data - assert(data.denom !== "") - assert(data.amount > 0) - assert(data.sender !== "") - assert(data.receiver !== "") +function onRecvPacket( + destChannelId: bytes, + sourceChannelId: bytes, + sequence: uint64, + payload: Payload, + relayer: address +): (bytes,bool) { + + appData=unmarshal(payload.encoding,payload.version,payload.appData) + abortTransactionUnless(appData!=nil) + + assert(appData.denom !== "") + assert(appData.amount > 0) + assert(appData.sender !== "") + assert(appData.receiver !== "") // construct default acknowledgement of success FungibleTokenPacketAcknowledgement ack = FungibleTokenPacketAcknowledgement{true, null} - prefix = "{packet.sourcePort}/{packet.sourceChannel}/" + prefix = "{payload.sourcePort}/{sourceChannelId}/" // we are the source if the packets were prefixed by the sending chain - source = data.denom.slice(0, len(prefix)) === prefix + source = appData.denom.slice(0, len(prefix)) === prefix if source { // receiver is source chain: unescrow tokens // determine escrow account - escrowAccount = channelEscrowAddresses[packet.destChannel] + escrowAccount = channelEscrowAddresses[destChannelId] // unescrow tokens to receiver (assumed to fail if balance insufficient) - err = bank.TransferCoins(escrowAccount, data.receiver, data.denom.slice(len(prefix)), data.amount) + err = bank.TransferCoins(escrowAccount, appData.receiver, appData.denom.slice(len(prefix)), appData.amount) if (err !== nil) ack = FungibleTokenPacketAcknowledgement{false, "transfer coins failed"} } else { prefix = "{packet.destPort}/{packet.destChannel}/" - prefixedDenomination = prefix + data.denom + prefixedDenomination = prefix + appData.denom // sender was source, mint vouchers to receiver (assumed to fail if balance insufficient) - err = bank.MintCoins(data.receiver, prefixedDenomination, data.amount) + err = bank.MintCoins(appData.receiver, prefixedDenomination, appData.amount) if (err !== nil) ack = FungibleTokenPacketAcknowledgement{false, "mint coins failed"} } - return ack + return ack,true } ``` @@ -322,48 +235,62 @@ function onRecvPacket(packet: Packet) { ```typescript function onAcknowledgePacket( - packet: Packet, - acknowledgement: bytes) { + sourceChannelId: bytes, + destChannelId: bytes, // This parameter won't be used. It's provided in input for adherence with ics04 + sequence: uint64, // This parameter won't be used. It's provided in input for adherence with ics04 + payload: Payload, + acknowledgement: bytes, + relayer: address + ): bool { // if the transfer failed, refund the tokens - if (!acknowledgement.success) - refundTokens(packet) + if (!acknowledgement.success){ + refundTokens(sourceChannelId,payload) + } + return true } ``` `onTimeoutPacket` is called by the routing module when a packet sent by this module has timed-out (such that it will not be received on the destination chain). ```typescript -function onTimeoutPacket(packet: Packet) { +function onTimeoutPacket( + sourceChannelId: bytes, + destChannelId: bytes, // This parameter won't be used. It's provided in input for adherence with ics04 + sequence: uint64, // This parameter won't be used. It's provided in input for adherence with ics04 + payload: Payload, + relayer: address + ): bool { // the packet timed-out, so refund the tokens - refundTokens(packet) + refundTokens(sourceChannelId,payload) + return true } ``` `refundTokens` is called by both `onAcknowledgePacket`, on failure, and `onTimeoutPacket`, to refund escrowed tokens to the original sender. ```typescript -function refundTokens(packet: Packet) { - FungibleTokenPacketData data = packet.data - prefix = "{packet.sourcePort}/{packet.sourceChannel}/" +function refundTokens( + sourceChannelId: bytes, + payload: Payload + ){ + + appData=unmarshal(payload.encoding,payload.version,payload.appData) + abortTransactionUnless(appData!=nil) + + prefix = "{payload.sourcePort}/{sourceChannelId}/" // we are the source if the denomination is not prefixed - source = data.denom.slice(0, len(prefix)) !== prefix + source = appData.denom.slice(0, len(prefix)) !== prefix if source { // sender was source chain, unescrow tokens back to sender - escrowAccount = channelEscrowAddresses[packet.srcChannel] - bank.TransferCoins(escrowAccount, data.sender, data.denom, data.amount) + escrowAccount = channelEscrowAddresses[sourceChannelId] + bank.TransferCoins(escrowAccount, appData.sender, appData.denom, appData.amount) } else { // receiver was source chain, mint vouchers back to sender - bank.MintCoins(data.sender, data.denom, data.amount) + bank.MintCoins(appData.sender, appData.denom, appData.amount) } } ``` -```typescript -function onTimeoutPacketClose(packet: Packet) { - // can't happen, only unordered channels allowed -} -``` - #### Using the Memo Field Note: Since earlier versions of this specification did not include a `memo` field, implementations must ensure that the new packet data is still compatible with chains that expect the old packet data. A legacy implementation MUST be able to unmarshal a new packet data with an empty string memo into the legacy `FungibleTokenPacketData` struct. Similarly, an implementation supporting `memo` must be able to unmarshal a legacy packet data into the current struct with the `memo` field set to the empty string. @@ -442,6 +369,8 @@ July 27, 2020 - Re-addition of source field Nov 11, 2022 - Addition of a memo field +Oct 31, 2024 - [Support for IBC TAO V2](https://github.com/cosmos/ibc/pull/1157) + ## Copyright All content herein is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/spec/client/ics-007-tendermint-client/README.md b/spec/client/ics-007-tendermint-client/README.md index f481a42c1..8386d3d35 100644 --- a/spec/client/ics-007-tendermint-client/README.md +++ b/spec/client/ics-007-tendermint-client/README.md @@ -8,7 +8,7 @@ implements: 2 version compatibility: ibc-go v7.0.0 author: Christopher Goes created: 2019-12-10 -modified: 2019-12-19 +modified: 2024-08-19 --- ## Synopsis @@ -59,7 +59,7 @@ This specification depends on correct instantiation of the [Tendermint consensus ### Client state -The Tendermint client state tracks the current revision, current validator set, trusting period, unbonding period, latest height, latest timestamp (block time), and a possible frozen height. +The Tendermint client state tracks the current revision, current validator set, trusting period, unbonding period, delayTimePeriod, delayBlockPeriod, latest height, latest timestamp (block time), and a possible frozen height. ```typescript interface ClientState { @@ -67,6 +67,8 @@ interface ClientState { trustLevel: Rational trustingPeriod: uint64 unbondingPeriod: uint64 + delayTimePeriod: uint64 + delayBlockPeriod: uint64 latestHeight: Height frozenHeight: Maybe upgradePath: []string @@ -377,8 +379,6 @@ These functions utilise the `proofSpecs` with which the client was initialised. function verifyMembership( clientState: ClientState, height: Height, - delayTimePeriod: uint64, - delayBlockPeriod: uint64, proof: CommitmentProof, path: CommitmentPath, value: []byte @@ -388,9 +388,9 @@ function verifyMembership( // check that the client is unfrozen or frozen at a higher height assert(clientState.frozenHeight === null || clientState.frozenHeight > height) // assert that enough time has elapsed - assert(currentTimestamp() >= processedTime + delayPeriodTime) + assert(currentTimestamp() >= processedTime + clientState.delayTimePeriod) // assert that enough blocks have elapsed - assert(currentHeight() >= processedHeight + delayPeriodBlocks) + assert(currentHeight() >= processedHeight + clientState.delayBlockPeriod) // fetch the previously verified commitment root & verify membership // Implementations may choose how to pass in the identifier // ibc-go provides the identifier-prefixed store to this method @@ -406,8 +406,6 @@ function verifyMembership( function verifyNonMembership( clientState: ClientState, height: Height, - delayTimePeriod: uint64, - delayBlockPeriod: uint64, proof: CommitmentProof, path: CommitmentPath ): Error { @@ -416,9 +414,9 @@ function verifyNonMembership( // check that the client is unfrozen or frozen at a higher height assert(clientState.frozenHeight === null || clientState.frozenHeight > height) // assert that enough time has elapsed - assert(currentTimestamp() >= processedTime + delayPeriodTime) + assert(currentTimestamp() >= processedTime + clientState.delayTimePeriod) // assert that enough blocks have elapsed - assert(currentHeight() >= processedHeight + delayPeriodBlocks) + assert(currentHeight() >= processedHeight + clientState.delayBlockPeriod) // fetch the previously verified commitment root & verify membership // Implementations may choose how to pass in the identifier // ibc-go provides the identifier-prefixed store to this method @@ -452,8 +450,11 @@ Not applicable. Alterations to the client verification algorithm will require a ## History December 10th, 2019 - Initial version + December 19th, 2019 - Final first draft +August 19th, 2024 - [Support for IBC/TAO V2](https://github.com/cosmos/ibc/pull/1137) + ## Copyright All content herein is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). diff --git a/spec/client/ics-007-tendermint-client/TAO_V1_README.md b/spec/client/ics-007-tendermint-client/TAO_V1_README.md new file mode 100644 index 000000000..f481a42c1 --- /dev/null +++ b/spec/client/ics-007-tendermint-client/TAO_V1_README.md @@ -0,0 +1,459 @@ +--- +ics: 7 +title: Tendermint Client +stage: draft +category: IBC/TAO +kind: instantiation +implements: 2 +version compatibility: ibc-go v7.0.0 +author: Christopher Goes +created: 2019-12-10 +modified: 2019-12-19 +--- + +## Synopsis + +This specification document describes a client (verification algorithm) for a blockchain using Tendermint consensus. + +### Motivation + +State machines of various sorts replicated using the Tendermint consensus algorithm might like to interface with other replicated state machines or solo machines over IBC. + +### Definitions + +Functions & terms are as defined in [ICS 2](../../core/ics-002-client-semantics). + +`currentTimestamp` is as defined in [ICS 24](../../core/ics-024-host-requirements). + +The Tendermint light client uses the generalised Merkle proof format as defined in [ICS 23](../../core/ics-023-vector-commitments). + +`hash` is a generic collision-resistant hash function, and can easily be configured. + +### Desired Properties + +This specification must satisfy the client interface defined in ICS 2. + +#### Note on "would-have-been-fooled logic + +The basic idea of "would-have-been-fooled" detection is that it allows us to be a bit more conservative, and freeze our light client when we know that another light client somewhere else on the network with a slightly different update pattern could have been fooled, even though we weren't. + +Consider a topology of three chains - `A`, `B`, and `C`, and two clients for chain `A`, `A_1` and `A_2`, running on chains `B` and `C` respectively. The following sequence of events occurs: + +- Chain `A` produces a block at height `h_0` (correctly). +- Clients `A_1` and `A_2` are updated to the block at height `h_0`. +- Chain `A` produces a block at height `h_0 + n` (correctly). +- Client `A_1` is updated to the block at height `h_0 + n` (client `A_2` is not yet updated). +- Chain `A` produces a second (equivocating) block at height `h_0 + k`, where `k <= n`. + +*Without* "would-have-been-fooled", it will be possible to freeze client `A_2` (since there are two valid blocks at height `h_0 + k` which are newer than the latest header `A_2` knows), but it will *not* be possible to freeze `A_1`, since `A_1` has already progressed beyond `h_0 + k`. + +Arguably, this is disadvantageous, since `A_1` was just "lucky" in having been updated when `A_2` was not, and clearly some Byzantine fault has happened that should probably be dealt with by human or governance system intervention. The idea of "would-have-been-fooled" is to allow this to be detected by having `A_1` start from a configurable past header to detect misbehaviour (so in this case, `A_1` would be able to start from `h_0` and would also be frozen). + +There is a free parameter here - namely, how far back is `A_1` willing to go (how big can `n` be where `A_1` will still be willing to look up `h_0`, having been updated to `h_0 + n`)? There is also a countervailing concern, in and of that double-signing is presumed to be costless after the unbonding period has passed, and we don't want to open up a denial-of-service vector for IBC clients. + +The necessary condition is thus that `A_1` should be willing to look up headers as old as it has stored, but should also enforce the "unbonding period" check on the misbehaviour, and avoid freezing the client if the misbehaviour is older than the unbonding period (relative to the client's local timestamp). If there are concerns about clock skew a slight delta could be added. + +## Technical Specification + +This specification depends on correct instantiation of the [Tendermint consensus algorithm](https://github.com/tendermint/spec/blob/master/spec/consensus/consensus.md) and [light client algorithm](https://github.com/tendermint/spec/blob/master/spec/light-client). + +### Client state + +The Tendermint client state tracks the current revision, current validator set, trusting period, unbonding period, latest height, latest timestamp (block time), and a possible frozen height. + +```typescript +interface ClientState { + chainID: string + trustLevel: Rational + trustingPeriod: uint64 + unbondingPeriod: uint64 + latestHeight: Height + frozenHeight: Maybe + upgradePath: []string + maxClockDrift: uint64 + proofSpecs: []ProofSpec +} +``` + +### Consensus state + +The Tendermint client tracks the timestamp (block time), the hash of the next validator set, and commitment root for all previously verified consensus states (these can be pruned after the unbonding period has passed, but should not be pruned beforehand). + +```typescript +interface ConsensusState { + timestamp: uint64 + nextValidatorsHash: []byte + commitmentRoot: []byte +} +``` + +### Height + +The height of a Tendermint client consists of two `uint64`s: the revision number, and the height in the revision. + +```typescript +interface Height { + revisionNumber: uint64 + revisionHeight: uint64 +} +``` + +Comparison between heights is implemented as follows: + +```typescript +function compare(a: TendermintHeight, b: TendermintHeight): Ord { + if (a.revisionNumber < b.revisionNumber) + return LT + else if (a.revisionNumber === b.revisionNumber) + if (a.revisionHeight < b.revisionHeight) + return LT + else if (a.revisionHeight === b.revisionHeight) + return EQ + return GT +} +``` + +This is designed to allow the height to reset to `0` while the revision number increases by one in order to preserve timeouts through zero-height upgrades. + +### Headers + +The Tendermint headers include the height, the timestamp, the commitment root, the hash of the validator set, the hash of the next validator set, and the signatures by the validators who committed the block. The header submitted to the on-chain client also includes the entire validator set, and a trusted height and validator set to update from. This reduces the amount of state maintained by the on-chain client and prevents race conditions on relayer updates. + +```typescript +interface TendermintSignedHeader { + height: uint64 + timestamp: uint64 + commitmentRoot: []byte + validatorsHash: []byte + nextValidatorsHash: []byte + signatures: []Signature +} +``` + +```typescript +interface Header { + TendermintSignedHeader + identifier: string + validatorSet: List> + trustedHeight: Height + trustedValidatorSet: List> +} + +// GetHeight will return the header Height in the IBC ClientHeight +// format. +// Implementations may use the revision number to increment the height +// across height-resetting upgrades. See ibc-go for an example +func (Header) GetHeight() { + return Height{0, height} +} +``` + +Header implements `ClientMessage` interface. + +### `Misbehaviour` + +The `Misbehaviour` type is used for detecting misbehaviour and freezing the client - to prevent further packet flow - if applicable. +Tendermint client `Misbehaviour` consists of two headers at the same height both of which the light client would have considered valid. + +```typescript +interface Misbehaviour { + identifier: string + h1: Header + h2: Header +} +``` + +Misbehaviour implements `ClientMessage` interface. + +### Client initialisation + +Tendermint client initialisation requires a (subjectively chosen) latest consensus state, including the full validator set. + +```typescript +function initialise( + identifier: Identifier, + clientState: ClientState, + consensusState: ConsensusState +) { + assert(clientState.trustingPeriod < clientState.unbondingPeriod) + assert(clientState.height > 0) + assert(clientState.trustLevel >= 1/3 && clientState.trustLevel <= 1) + + provableStore.set("clients/{identifier}/clientState", clientState) + provableStore.set("clients/{identifier}/consensusStates/{height}", consensusState) +} +``` + +The Tendermint client `latestClientHeight` function returns the latest stored height, which is updated every time a new (more recent) header is validated. + +```typescript +function latestClientHeight(clientState: ClientState): Height { + return clientState.latestHeight +} +``` + +### Validity predicate + +Tendermint client validity checking uses the bisection algorithm described in the [Tendermint spec](https://github.com/tendermint/spec/tree/master/spec/consensus/light-client). If the provided header is valid, the client state is updated & the newly verified commitment written to the store. + +```typescript +function verifyClientMessage(clientMsg: ClientMessage) { + switch typeof(clientMsg) { + case Header: + verifyHeader(clientMsg) + case Misbehaviour: + verifyHeader(clientMsg.h1) + verifyHeader(clientMsg.h2) + } +} +``` + +Verify validity of regular update to the Tendermint client + +```typescript +function verifyHeader(header: Header) { + clientState = provableStore.get("clients/{header.identifier}/clientState") + // assert trusting period has not yet passed + assert(currentTimestamp() - clientState.latestTimestamp < clientState.trustingPeriod) + // assert header timestamp is less than trust period in the future. This should be resolved with an intermediate header. + assert(header.timestamp - clientState.latestTimeStamp < clientState.trustingPeriod) + // trusted height revision must be the same as header revision + // if revisions are different, use upgrade client instead + // trusted height must be less than header height + assert(header.GetHeight().revisionNumber == header.trustedHeight.revisionNumber) + assert(header.GetHeight().revisionHeight > header.trustedHeight.revisionHeight) + // fetch the consensus state at the trusted height + consensusState = provableStore.get("clients/{header.identifier}/consensusStates/{header.trustedHeight}") + // assert that header's trusted validator set hashes to consensus state's validator hash + assert(hash(header.trustedValidatorSet) == consensusState.nextValidatorsHash) + + // call the tendermint client's `verify` function + assert(tmClient.verify( + header.trustedValidatorSet, + clientState.latestHeight, + clientState.trustingPeriod, + clientState.maxClockDrift, + header.TendermintSignedHeader, + )) +} +``` + +### Misbehaviour predicate + +Function `checkForMisbehaviour` will check if an update contains evidence of Misbehaviour. If the ClientMessage is a header we check for implicit evidence of misbehaviour by checking if there already exists a conflicting consensus state in the store or if the header breaks time monotonicity. + +```typescript +function checkForMisbehaviour(clientMsg: clientMessage): boolean { + clientState = provableStore.get("clients/{clientMsg.identifier}/clientState") + switch typeof(clientMsg) { + case Header: + // fetch consensusstate at header height if it exists + consensusState = provableStore.get("clients/{clientMsg.identifier}/consensusStates/{header.GetHeight()}") + // if consensus state exists and conflicts with the header + // then the header is evidence of misbehaviour + if consensusState != nil && + !( + consensusState.timestamp == header.timestamp && + consensusState.commitmentRoot == header.commitmentRoot && + consensusState.nextValidatorsHash == header.nextValidatorsHash + ) { + return true + } + + // check for time monotonicity misbehaviour + // if header is not monotonically increasing with respect to neighboring consensus states + // then return true + // NOTE: implementation must have ability to iterate ascending/descending by height + prevConsState = getPreviousConsensusState(header.GetHeight()) + nextConsState = getNextConsensusState(header.GetHeight()) + if prevConsState.timestamp >= header.timestamp { + return true + } + if nextConsState != nil && nextConsState.timestamp <= header.timestamp { + return true + } + case Misbehaviour: + if (misbehaviour.h1.height < misbehaviour.h2.height) { + return false + } + // if heights are equal check that this is valid misbehaviour of a fork + if (misbehaviour.h1.height === misbehaviour.h2.height && misbehaviour.h1.commitmentRoot !== misbehaviour.h2.commitmentRoot) { + return true + } + // otherwise if heights are unequal check that this is valid misbehavior of BFT time violation + if (misbehaviour.h1.timestamp <= misbehaviour.h2.timestamp) { + return true + } + + return false + } +} +``` + +### Update state + +Function `updateState` will perform a regular update for the Tendermint client. It will add a consensus state to the client store. If the header is higher than the latest height on the `clientState`, then the `clientState` will be updated. + +```typescript +function updateState(clientMsg: clientMessage) { + clientState = provableStore.get("clients/{clientMsg.identifier}/clientState") + header = Header(clientMessage) + // only update the clientstate if the header height is higher + // than clientState latest height + if clientState.height < header.GetHeight() { + // update latest height + clientState.latestHeight = header.GetHeight() + + // save the client + provableStore.set("clients/{clientMsg.identifier}/clientState", clientState) + } + + // create recorded consensus state, save it + consensusState = ConsensusState{header.timestamp, header.nextValidatorsHash, header.commitmentRoot} + provableStore.set("clients/{clientMsg.identifier}/consensusStates/{header.GetHeight()}", consensusState) + + // these may be stored as private metadata within the client in order to verify + // that the delay period has passed in proof verification + provableStore.set("clients/{clientMsg.identifier}/processedTimes/{header.GetHeight()}", currentTimestamp()) + provableStore.set("clients/{clientMsg.identifier}/processedHeights/{header.GetHeight()}", currentHeight()) +} +``` + +### Update state on misbehaviour + +Function `updateStateOnMisbehaviour` will set the frozen height to a non-zero sentinel height to freeze the entire client. + +```typescript +function updateStateOnMisbehaviour(clientMsg: clientMessage) { + clientState = provableStore.get("clients/{clientMsg.identifier}/clientState") + clientState.frozenHeight = Height{0, 1} + provableStore.set("clients/{clientMsg.identifier}/clientState", clientState) +} +``` + +### Upgrades + +The chain which this light client is tracking can elect to write a special pre-determined key in state to allow the light client to update its client state (e.g. with a new chain ID or revision) in preparation for an upgrade. + +As the client state change will be performed immediately, once the new client state information is written to the predetermined key, the client will no longer be able to follow blocks on the old chain, so it must upgrade promptly. + +```typescript +function upgradeClientState( + clientState: ClientState, + newClientState: ClientState, + height: Height, + proof: CommitmentProof +) { + // assert trusting period has not yet passed + assert(currentTimestamp() - clientState.latestTimestamp < clientState.trustingPeriod) + // check that the revision has been incremented + assert(newClientState.latestHeight.revisionNumber > clientState.latestHeight.revisionNumber) + // check proof of updated client state in state at predetermined commitment prefix and key + path = applyPrefix(clientState.upgradeCommitmentPrefix, clientState.upgradeKey) + // check that the client is at a sufficient height + assert(clientState.latestHeight >= height) + // check that the client is unfrozen or frozen at a higher height + assert(clientState.frozenHeight === null || clientState.frozenHeight > height) + // fetch the previously verified commitment root & verify membership + // Implementations may choose how to pass in the identifier + // ibc-go provides the identifier-prefixed store to this method + // so that all state reads are for the client in question + consensusState = provableStore.get("clients/{clientIdentifier}/consensusStates/{height}") + // verify that the provided consensus state has been stored + assert(verifyMembership(consensusState.commitmentRoot, proof, path, newClientState)) + // update client state + clientState = newClientState + provableStore.set("clients/{clientIdentifier}/clientState", clientState) +} +``` + +### State verification functions + +Tendermint client state verification functions check a Merkle proof against a previously validated commitment root. + +These functions utilise the `proofSpecs` with which the client was initialised. + +```typescript +function verifyMembership( + clientState: ClientState, + height: Height, + delayTimePeriod: uint64, + delayBlockPeriod: uint64, + proof: CommitmentProof, + path: CommitmentPath, + value: []byte +): Error { + // check that the client is at a sufficient height + assert(clientState.latestHeight >= height) + // check that the client is unfrozen or frozen at a higher height + assert(clientState.frozenHeight === null || clientState.frozenHeight > height) + // assert that enough time has elapsed + assert(currentTimestamp() >= processedTime + delayPeriodTime) + // assert that enough blocks have elapsed + assert(currentHeight() >= processedHeight + delayPeriodBlocks) + // fetch the previously verified commitment root & verify membership + // Implementations may choose how to pass in the identifier + // ibc-go provides the identifier-prefixed store to this method + // so that all state reads are for the client in question + consensusState = provableStore.get("clients/{clientIdentifier}/consensusStates/{height}") + // verify that has been stored + if !verifyMembership(consensusState.commitmentRoot, proof, path, value) { + return error + } + return nil +} + +function verifyNonMembership( + clientState: ClientState, + height: Height, + delayTimePeriod: uint64, + delayBlockPeriod: uint64, + proof: CommitmentProof, + path: CommitmentPath +): Error { + // check that the client is at a sufficient height + assert(clientState.latestHeight >= height) + // check that the client is unfrozen or frozen at a higher height + assert(clientState.frozenHeight === null || clientState.frozenHeight > height) + // assert that enough time has elapsed + assert(currentTimestamp() >= processedTime + delayPeriodTime) + // assert that enough blocks have elapsed + assert(currentHeight() >= processedHeight + delayPeriodBlocks) + // fetch the previously verified commitment root & verify membership + // Implementations may choose how to pass in the identifier + // ibc-go provides the identifier-prefixed store to this method + // so that all state reads are for the client in question + consensusState = provableStore.get("clients/{clientIdentifier}/consensusStates/{height}") + // verify that nothing has been stored at path + if !verifyNonMembership(consensusState.commitmentRoot, proof, path) { + return error + } + return nil +} +``` + +### Properties & Invariants + +Correctness guarantees as provided by the Tendermint light client algorithm. + +## Backwards Compatibility + +Not applicable. + +## Forwards Compatibility + +Not applicable. Alterations to the client verification algorithm will require a new client standard. + +## Example Implementations + +- Implementation of ICS 07 in Go can be found in [ibc-go repository](https://github.com/cosmos/ibc-go). +- Implementation of ICS 07 in Rust can be found in [ibc-rs repository](https://github.com/cosmos/ibc-rs). + +## History + +December 10th, 2019 - Initial version +December 19th, 2019 - Final first draft + +## Copyright + +All content herein is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0).