From 517963dffd1dce45127b8a49b1ef971f147d5dd6 Mon Sep 17 00:00:00 2001 From: Aditya Sripal Date: Thu, 4 Aug 2022 14:37:01 +0200 Subject: [PATCH 01/10] split verify and update functions into 4 functions defined in ADR --- spec/core/ics-002-client-semantics/README.md | 145 +++++++++++-------- 1 file changed, 82 insertions(+), 63 deletions(-) diff --git a/spec/core/ics-002-client-semantics/README.md b/spec/core/ics-002-client-semantics/README.md index b651e5e45..5851a93b2 100644 --- a/spec/core/ics-002-client-semantics/README.md +++ b/spec/core/ics-002-client-semantics/README.md @@ -152,6 +152,31 @@ State machines implementing the IBC protocol are expected to respect these clien ### 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 @@ -180,14 +205,39 @@ The `ConsensusState` MUST define a `getTimestamp()` method which returns the tim type getTimestamp = ConsensusState => uint64 ``` -#### Header +#### 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 a provided consensus state, writing to internal state as appropriate. + +```typescript +type initialise = (consensusState: ConsensusState) => ClientState +``` + +Client types MUST define a method to fetch the current height (height of the most recent validated header). -A `Header` is an opaque data structure defined by a client type which provides information to update a `ConsensusState`. -Headers can be submitted to an associated client to update the stored `ConsensusState`. They likely contain a height, a proof, -a commitment root, and possibly updates to the validity predicate. +```typescript +type latestClientHeight = ( + clientState: ClientState) + => Height +``` + +#### ClientMessage + +A `ClientMessage` is an opaque data structure defined by a client type which provides information to update the client. +`ClientMessages` 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 Header = bytes +type ClientMessage = bytes ``` #### Validity predicate @@ -196,95 +246,64 @@ A validity predicate is an opaque function defined by a client type to verify `H Using the validity predicate SHOULD be far more computationally efficient than replaying the full consensus algorithm for the given parent `Header` and the list of network messages. -The validity predicate & client state update logic are combined into a single `checkValidityAndUpdateState` type, which is defined as +The validity predicate is defined as: ```typescript -type checkValidityAndUpdateState = (Header) => Void +type VerifyClientMessage = (ClientMessage) => Void ``` -`checkValidityAndUpdateState` MUST throw an exception if the provided header was not valid. - -If the provided header was valid, the client MUST also mutate internal state to store -now-finalised consensus roots and update any necessary signature authority tracking (e.g. -changes to the validator set) for future calls to the validity predicate. - -Clients MAY have time-sensitive validity predicates, such that if no header is provided for a period of time -(e.g. an unbonding period of three weeks) it will no longer be possible to update the client. -In this case, a permissioned entity such as a chain governance system or trusted multi-signature MAY be allowed -to intervene to unfreeze a frozen client & provide a new correct header. +`VerifyClientMessage` MUST throw an exception if the provided ClientMessage was not valid. #### Misbehaviour predicate -A misbehaviour predicate is an opaque function defined by a client type, used to check if data +A misbehaviour predicate is an opaque function defined by a client type, used to check if a ClientMessage constitutes a violation of the consensus protocol. This might be two signed headers with different state roots but the same height, a signed header containing invalid state transitions, or other proof of malfeasance as defined by the consensus algorithm. -The misbehaviour predicate & client state update logic are combined into a single `checkMisbehaviourAndUpdateState` type, which is defined as +The misbehaviour predicate is defined as ```typescript -type checkMisbehaviourAndUpdateState = (bytes) => Void +type checkForMisbehaviour = (ClientMessage) => bool ``` -`checkMisbehaviourAndUpdateState` MUST throw an exception if the provided proof of misbehaviour was not valid. - -If misbehaviour was valid, the client MUST also mutate internal state to mark appropriate heights which -were previously considered valid as invalid, according to the nature of the misbehaviour. +`checkForMisbehaviour` MUST throw an exception if the provided proof of misbehaviour was not valid. -Once misbehaviour is detected, clients SHOULD be frozen so that no future updates can be submitted. -A permissioned entity such as a chain governance system or trusted multi-signature MAY be allowed -to intervene to unfreeze a frozen client & provide a new correct header. - -#### Height - -`Height` is an opaque data structure defined by a client type. -It must form a partially ordered set & provide operations for comparison. +#### UpdateState -```typescript -type Height -``` +UpdateState will update the client given a verified `ClientMessage`. Note that this function is intended for **non-misbehaviour** `ClientMessages`. ```typescript -enum Ord { - LT - EQ - GT -} - -type compare = (h1: Height, h2: Height) => Ord +type UpdateState = (ClientMessage) => Void ``` -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`. +`verifyClientMessage` must be called before this function, and `checkForMisbehaviour` must return false before this function is called. -There must also be a zero-element for a height type, referred to as `0`, which is less than all non-zero heights. +The client MUST also mutate internal state to store +now-finalised consensus roots and update any necessary signature authority tracking (e.g. +changes to the validator set) for future calls to the validity predicate. -#### ClientState +Clients MAY have time-sensitive validity predicates, such that if no ClientMessage is provided for a period of time +(e.g. an unbonding period of three weeks) it will no longer be possible to update the client. +In this case, a permissioned entity such as a chain governance system or trusted multi-signature MAY be allowed +to intervene to unfreeze a frozen client & provide a new correct ClientMessage. -`ClientState` is an opaque data structure defined by a client type. -It may keep arbitrary internal state to track verified roots and past misbehaviours. +#### UpdateStateOnMisbehaviour -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. +UpdateStateOnMisbehaviour will update the client upon receiving a verified `ClientMessage` that is valid misbehaviour. ```typescript -type ClientState = bytes +type UpdateStateOnMisbehaviour = (ClientMessage) => Void ``` -Client types MUST define a method to initialise a client state with a provided consensus state, writing to internal state as appropriate. - -```typescript -type initialise = (consensusState: ConsensusState) => ClientState -``` +`verifyClientMessage` must be called before this function, and `checkForMisbehaviour` must return `true` before this function is called. -Client types MUST define a method to fetch the current height (height of the most recent validated header). +The client MUST also mutate internal state to mark appropriate heights which +were previously considered valid as invalid, according to the nature of the misbehaviour. -```typescript -type latestClientHeight = ( - clientState: ClientState) - => Height -``` +Once misbehaviour is detected, clients SHOULD be frozen so that no future updates can be submitted. +A permissioned entity such as a chain governance system or trusted multi-signature MAY be allowed +to intervene to unfreeze a frozen client & provide a new correct ClientMessage which updates the client to a valid state. #### CommitmentProof From 2d5427c53312bb86879533e55d0d226acd6d8967 Mon Sep 17 00:00:00 2001 From: Aditya Sripal Date: Thu, 4 Aug 2022 17:49:38 +0200 Subject: [PATCH 02/10] verify functions and example changes --- spec/core/ics-002-client-semantics/README.md | 360 +++++++------------ 1 file changed, 124 insertions(+), 236 deletions(-) diff --git a/spec/core/ics-002-client-semantics/README.md b/spec/core/ics-002-client-semantics/README.md index 5851a93b2..f0eed0cd8 100644 --- a/spec/core/ics-002-client-semantics/README.md +++ b/spec/core/ics-002-client-semantics/README.md @@ -6,9 +6,9 @@ category: IBC/TAO kind: interface requires: 23, 24 required-by: 3 -author: Juwoon Yun , Christopher Goes +author: Juwoon Yun , Christopher Goes , Aditya Sripal created: 2019-02-25 -modified: 2020-01-13 +modified: 2022-08-04 --- ## Synopsis @@ -316,129 +316,11 @@ at a particular finalised height (necessarily associated with a particular commi 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). -- The `delayPeriodTime` is passed to packet-related verification functions in order to allow packets to specify a period of time which must pass after a header is verified before the packet is allowed to be processed. -- The `delayPeriodBlocks` is passed to packet-related verification functions in order to allow packets to specify a period of blocks which must pass after a header is verified before the packet is allowed to be processed. +- The `delayPeriodTime` is passed to the verification functions for packet-related proofs in order to allow packets to specify a period of time which must pass after a header is verified before the packet is allowed to be processed. +- The `delayPeriodBlocks` is passed to the verification functions for packet-related proofs in order to allow packets to specify a period of blocks which must pass after a header is verified before the packet is allowed to be processed. ##### Required functions -`verifyClientConsensusState` verifies a proof of the consensus state of the specified client stored on the target state machine. - -```typescript -type verifyClientConsensusState = ( - clientState: ClientState, - height: Height, - proof: CommitmentProof, - clientIdentifier: Identifier, - consensusStateHeight: Height, - consensusState: ConsensusState) - => boolean -``` - -`verifyClientState` verifies a proof of the client state of the specified client stored on the target machine. - -```typescript -type verifyClientState = ( - height: Height, - prefix: CommitmentPrefix, - proof: CommitmentProof, - clientIdentifier: Identifier, - clientState: ClientState) - => boolean -``` - -`verifyConnectionState` verifies a proof of the connection state of the specified connection end stored on the target state machine. - -```typescript -type verifyConnectionState = ( - clientState: ClientState, - height: Height, - prefix: CommitmentPrefix, - proof: CommitmentProof, - connectionIdentifier: Identifier, - connectionEnd: ConnectionEnd) - => boolean -``` - -`verifyChannelState` verifies a proof of the channel state of the specified channel end, under the specified port, stored on the target state machine. - -```typescript -type verifyChannelState = ( - clientState: ClientState, - height: Height, - prefix: CommitmentPrefix, - proof: CommitmentProof, - portIdentifier: Identifier, - channelIdentifier: Identifier, - channelEnd: ChannelEnd) - => boolean -``` - -`verifyPacketData` verifies a proof of an outgoing packet commitment at the specified port, specified channel, and specified sequence. - -```typescript -type verifyPacketData = ( - clientState: ClientState, - height: Height, - delayPeriodTime: uint64, - delayPeriodBlocks: uint64, - prefix: CommitmentPrefix, - proof: CommitmentProof, - portIdentifier: Identifier, - channelIdentifier: Identifier, - sequence: uint64, - data: bytes) - => boolean -``` - -`verifyPacketAcknowledgement` verifies a proof of an incoming packet acknowledgement at the specified port, specified channel, and specified sequence. - -```typescript -type verifyPacketAcknowledgement = ( - clientState: ClientState, - height: Height, - delayPeriodTime: uint64, - delayPeriodBlocks: uint64, - prefix: CommitmentPrefix, - proof: CommitmentProof, - portIdentifier: Identifier, - channelIdentifier: Identifier, - sequence: uint64, - acknowledgement: bytes) - => boolean -``` - -`verifyPacketReceiptAbsence` verifies a proof of the absence of an incoming packet receipt at the specified port, specified channel, and specified sequence. - -```typescript -type verifyPacketReceiptAbsence = ( - clientState: ClientState, - height: Height, - delayPeriodTime: uint64, - delayPeriodBlocks: uint64, - prefix: CommitmentPrefix, - proof: CommitmentProof, - portIdentifier: Identifier, - channelIdentifier: Identifier, - sequence: uint64) - => boolean -``` - -`verifyNextSequenceRecv` verifies a proof of the next sequence number to be received of the specified channel at the specified port. - -```typescript -type verifyNextSequenceRecv = ( - clientState: ClientState, - height: Height, - delayPeriodTime: uint64, - delayPeriodBlocks: uint64, - prefix: CommitmentPrefix, - proof: CommitmentProof, - portIdentifier: Identifier, - channelIdentifier: Identifier, - nextSequenceRecv: uint64) - => boolean -``` - `verifyMembership` is a generic proof verification method which verifies a proof of the existence of a value at a given `CommitmentPath` at the specified height. The caller is expected to construct the full `CommitmentPath` from a `CommitmentPrefix` and a standardized path (as defined in [ICS 24](../ics-024-host-requirements/README.md#path-space)). If the caller desires a particular delay period to be enforced, then it can pass in a non-zero `delayPeriodTime` or `delayPeriodBlocks`. If a delay period is not necessary, the caller must pass in 0 for `delayPeriodTime` and `delayPeriodBlocks`, @@ -474,7 +356,6 @@ type verifyNonMembership = ( => boolean ``` - #### Query interface ##### Chain queries @@ -679,29 +560,43 @@ to allow governance mechanisms to perform these actions ```typescript function updateClient( id: Identifier, - header: Header) { - clientType = provableStore.get(clientTypePath(id)) - abortTransactionUnless(clientType !== null) - clientState = provableStore.get(clientStatePath(id)) - abortTransactionUnless(clientState !== null) - clientType.checkValidityAndUpdateState(clientState, header) + clientMessage: ClientMessage) { + if err := clientState.VerifyClientMessage(clientMessage); err != nil { + return err + } + + foundMisbehaviour := clientState.CheckForMisbehaviour(clientMessage) + if foundMisbehaviour { + clientState.UpdateStateOnMisbehaviour(header) + // emit misbehaviour event + return + } + + clientState.UpdateState(clientMessage) // expects no-op on duplicate header + // emit update event + return } ``` #### Misbehaviour -If the client detects proof of misbehaviour, the client can be alerted, possibly invalidating +If a relayer wishes to explicitly submit misbehaviour. They may alert the client to the misbehaviour directly, possibly invalidating previously valid state roots & preventing future updates. ```typescript function submitMisbehaviourToClient( id: Identifier, - misbehaviour: bytes) { + clientMessage: ClientMessage) { clientType = provableStore.get(clientTypePath(id)) abortTransactionUnless(clientType !== null) clientState = provableStore.get(clientStatePath(id)) abortTransactionUnless(clientState !== null) - clientType.checkMisbehaviourAndUpdateState(clientState, misbehaviour) + // authenticate client message + clientState.verifyClientMessage(clientMessage) + // check that client message is valid instance of misbehaviour + clientState.checkForMisbehaviour(clientMessage) + // update state based on misbehaviour + clientState.UpdateStateOnMisbehaviour(misbehaviour) } ``` @@ -714,9 +609,12 @@ can be changed while the chain is running. The client-specific types are then defined as follows: - `ConsensusState` stores the latest height and latest public key -- `Header`s contain a height, a new commitment root, a signature by the operator, and possibly a new public key -- `checkValidityAndUpdateState` checks that the submitted height is monotonically increasing and that the signature is correct, then mutates the internal state -- `checkMisbehaviourAndUpdateState` checks for two headers with the same height & different commitment roots, then mutates the internal state +- `ClientMessage` for UpdateState contain a height, a new commitment root, a signature by the operator, and possibly a new public key +- `ClientMessage` for UpdateStateForMisbehaviour contains two conflicting commitment roots signed by the same operator at the same height +- `verifyClientMessage` verifies that the operator did sign the given commitment root at the given height +- `checkForMisbehaviour` verifies that the two commitment roots in ClientMessage are different for the same height +- `checkValidityAndUpdateState` checks that the submitted height is monotonically increasing, then mutates the internal state +- `checkMisbehaviourAndUpdateState` mutates the internal state for misbehaviour ```typescript type Height = uint64 @@ -730,10 +628,19 @@ function compare(h1: Height, h2: Height): Ord { return GT } +// information on when a header was received by the client +// this will be used to verify delay period has passed +// during proof verification +interface MessageReceiptInfo{ + height: Height + time: uint64 +} + interface ClientState { frozen: boolean pastPublicKeys: Set verifiedRoots: Map + messageReceipts: Map } interface ConsensusState { @@ -748,9 +655,8 @@ interface Header { newPublicKey: Maybe } -interface Misbehaviour { - h1: Header - h2: Header +interface ClientMessage { + headers: List
} // algorithm run by operator to commit a new block @@ -774,133 +680,113 @@ function initialise(consensusState: ConsensusState): () { } // validity predicate function defined by the client type -function checkValidityAndUpdateState( +function verifyClientMessage( clientState: ClientState, - header: Header) { - abortTransactionUnless(consensusState.sequence + 1 === header.sequence) - abortTransactionUnless(consensusState.publicKey.verify(header.signature)) - if (header.newPublicKey !== null) { - consensusState.publicKey = header.newPublicKey - clientState.pastPublicKeys.add(header.newPublicKey) + clientMessage: ClientMessage, +) { + for h in clientMessage.headers { + abortTransactionUnless(consensusState.publicKey.verify(h.signature)) } - consensusState.sequence = header.sequence - clientState.verifiedRoots[sequence] = header.commitmentRoot } -function verifyClientConsensusState( +// misbehaviour predicate function defined by the client type +function checkForMisbehaviour( clientState: ClientState, - height: Height, - prefix: CommitmentPrefix, - proof: CommitmentProof, - clientIdentifier: Identifier, - consensusState: ConsensusState) { - path = applyPrefix(prefix, "clients/{clientIdentifier}/consensusStates/{height}") - abortTransactionUnless(!clientState.frozen) - return clientState.verifiedRoots[sequence].verifyMembership(path, consensusState, proof) -} - -function verifyConnectionState( - clientState: ClientState, - height: Height, - prefix: CommitmentPrefix, - proof: CommitmentProof, - connectionIdentifier: Identifier, - connectionEnd: ConnectionEnd) { - path = applyPrefix(prefix, "connections/{connectionIdentifier}") - abortTransactionUnless(!clientState.frozen) - return clientState.verifiedRoots[sequence].verifyMembership(path, connectionEnd, proof) + clientMessage: ClientMessage, +) bool { + if len(clientMessage.Headers != 2) { + return false + } + // headers must be at same height + if clientMessage.headers[0].sequence != clientMessage.headers[1].sequence { + return false + } + // headers must have different roots + if clientMessage.headers[1].commitmentRoot == clientMessage.headers[1].commitmentRoot { + return false + } + return true } -function verifyChannelState( +// updateState function defined by the client type +function updateState( clientState: ClientState, - height: Height, - prefix: CommitmentPrefix, - proof: CommitmentProof, - portIdentifier: Identifier, - channelIdentifier: Identifier, - channelEnd: ChannelEnd) { - path = applyPrefix(prefix, "ports/{portIdentifier}/channels/{channelIdentifier}") - abortTransactionUnless(!clientState.frozen) - return clientState.verifiedRoots[sequence].verifyMembership(path, channelEnd, proof) + clientMessage: ClientMessage) { + // supports 1 update at a time + abortTransactionUnless(len(clientMessage.headers) = 1) + header = clientMessage.headers[0] + abortTransactionUnless(consensusState.sequence + 1 === header.sequence) + if (header.newPublicKey !== null) { + consensusState.publicKey = header.newPublicKey + clientState.pastPublicKeys.add(header.newPublicKey) + } + consensusState.sequence = header.sequence + clientState.verifiedRoots[sequence] = header.commitmentRoot + // store the height and time at which the client received the header + // so we can ensure that delay period has passed before we use this header for proof verification + messageReceipt = MessageReceiptInfo{ + height: currentHeight(), + time: currentTimestamp() + } + clientState.messageReceipts[sequence] = messageReceipt } -function verifyPacketData( +// updateStateOnMisbehaviour function defined by the client type +function updateStateOnMisbehaviour( clientState: ClientState, - height: Height, - delayPeriodTime: uint64, - delayPeriodBlocks: uint64, - prefix: CommitmentPrefix, - proof: CommitmentProof, - portIdentifier: Identifier, - channelIdentifier: Identifier, - sequence: uint64, - data: bytes) { - path = applyPrefix(prefix, "ports/{portIdentifier}/channels/{channelIdentifier}/packets/{sequence}") - abortTransactionUnless(!clientState.frozen) - return clientState.verifiedRoots[sequence].verifyMembership(path, hash(data), proof) + clientMessage: ClientMessage) { + clientState.frozen = true } -function verifyPacketAcknowledgement( +// verifyMembership function defined by the client type +function verifyMembership( clientState: ClientState, height: Height, delayPeriodTime: uint64, delayPeriodBlocks: uint64, - prefix: CommitmentPrefix, proof: CommitmentProof, - portIdentifier: Identifier, - channelIdentifier: Identifier, - sequence: uint64, - acknowledgement: bytes) { - path = applyPrefix(prefix, "ports/{portIdentifier}/channels/{channelIdentifier}/acknowledgements/{sequence}") + path: CommitmentPath, + value: bytes) => boolean { abortTransactionUnless(!clientState.frozen) - return clientState.verifiedRoots[sequence].verifyMembership(path, hash(acknowledgement), proof) + if delayPeriodBlocks != 0 { + // ensure that `delayPeriodBlocks` has passed since receiving the header for this height + // before using in proof verification + receivedHeight = clientState.messageReceipts[height].height + abortTransactionUnless(currentHeight() - receivedHeight >= delayPeriodBlocks) + } + if delayPeriodTime != 0 { + // ensure that `delayPeriodTime` has passed since receiving the header for this height + // before using in proof verification + receivedTime = clientState.messageReceipts[height].time + abortTransactionUnless(currentTimestamp() - receivedTime >= delayPeriodTime) + } + return clientState.verifiedRoots[height].verifyMembership(path, value, proof) } -function verifyPacketReceiptAbsence( +// verifyNonMembership function defined by the client type +function verifyNonMembership( clientState: ClientState, height: Height, prefix: CommitmentPrefix, delayPeriodTime: uint64, delayPeriodBlocks: uint64, proof: CommitmentProof, - portIdentifier: Identifier, - channelIdentifier: Identifier, - sequence: uint64) { - path = applyPrefix(prefix, "ports/{portIdentifier}/channels/{channelIdentifier}/receipts/{sequence}") - abortTransactionUnless(!clientState.frozen) - return clientState.verifiedRoots[sequence].verifyNonMembership(path, proof) -} - -function verifyNextSequenceRecv( - clientState: ClientState, - height: Height, - delayPeriodTime: uint64, - delayPeriodBlocks: uint64, - prefix: CommitmentPrefix, - proof: CommitmentProof, - portIdentifier: Identifier, - channelIdentifier: Identifier, - nextSequenceRecv: uint64) { - path = applyPrefix(prefix, "ports/{portIdentifier}/channels/{channelIdentifier}/nextSequenceRecv") + path) => boolean { abortTransactionUnless(!clientState.frozen) - return clientState.verifiedRoots[sequence].verifyMembership(path, nextSequenceRecv, proof) -} - -// misbehaviour verification function defined by the client type -// any duplicate signature by a past or current key freezes the client -function checkMisbehaviourAndUpdateState( - clientState: ClientState, - misbehaviour: Misbehaviour) { - h1 = misbehaviour.h1 - h2 = misbehaviour.h2 - abortTransactionUnless(clientState.pastPublicKeys.contains(h1.publicKey)) - abortTransactionUnless(h1.sequence === h2.sequence) - abortTransactionUnless(h1.commitmentRoot !== h2.commitmentRoot || h1.publicKey !== h2.publicKey) - abortTransactionUnless(h1.publicKey.verify(h1.signature)) - abortTransactionUnless(h2.publicKey.verify(h2.signature)) - clientState.frozen = true + if delayPeriodBlocks != 0 { + // ensure that `delayPeriodBlocks` has passed since receiving the header for this height + // before using in proof verification + receivedHeight = clientState.messageReceipts[height].height + abortTransactionUnless(currentHeight() - receivedHeight >= delayPeriodBlocks) + } + if delayPeriodTime != 0 { + // ensure that `delayPeriodTime` has passed since receiving the header for this height + // before using in proof verification + receivedTime = clientState.messageReceipts[height].time + abortTransactionUnless(currentTimestamp() - receivedTime >= delayPeriodTime) + } + return clientState.verifiedRoots[height].verifyNonMembership(path, proof) } -``` ### Properties & Invariants @@ -936,6 +822,8 @@ 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: https://github.com/cosmos/ibc-go/pull/1871 + ## Copyright All content herein is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). From 5a339776630dd10839d19b3b95124a830ffe2f83 Mon Sep 17 00:00:00 2001 From: Aditya Sripal Date: Thu, 4 Aug 2022 18:01:34 +0200 Subject: [PATCH 03/10] add getTimestampAtHeight to ClientState interface --- spec/core/ics-002-client-semantics/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/core/ics-002-client-semantics/README.md b/spec/core/ics-002-client-semantics/README.md index f0eed0cd8..4f4479139 100644 --- a/spec/core/ics-002-client-semantics/README.md +++ b/spec/core/ics-002-client-semantics/README.md @@ -231,6 +231,15 @@ type latestClientHeight = ( => 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. From 82129c7ddf6a3e220a7f2ed31ebc332bc5fc0607 Mon Sep 17 00:00:00 2001 From: Aditya Sripal Date: Thu, 4 Aug 2022 18:05:29 +0200 Subject: [PATCH 04/10] changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8804e72d..cee1e0d0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,14 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] +### API-Breaking + +- [\#813](https://github.com/cosmos/ibc/pull/813) Breaks up `checkValidityAndUpdateState` into `verifyClientMessage` and `UpdateState` +- [\#813](https://github.com/cosmos/ibc/pull/813) Breaks up `checkMisbehaviourAndUpdateState` into `checkForMisbehaviour` and `UpdateStateOnMisbehaviour` +- [\#813](https://github.com/cosmos/ibc/pull/813) Removes `Header` and `Misbehaviour` interfaces for generic `ClientMessage` interface +- [\#813](https://github.com/cosmos/ibc/pull/813) Removes specific verify functions from ClientState interface in exchange for generic `verifyMembership` and `verifyNonMembership` methods +- [\#813](https://github.com/cosmos/ibc/pull/813) Adds `getTimeoutAtHeight` method to ClientState interface. + ### Bug Fixes - [\#808](https://github.com/cosmos/ibc/pull/808) Fix channel sequence paths in ICS4 From da71ea84bdd215fd1bebb0578ff55f791c218e66 Mon Sep 17 00:00:00 2001 From: Aditya Date: Wed, 17 Aug 2022 15:17:49 +0200 Subject: [PATCH 05/10] Apply suggestions from code review Co-authored-by: Marius Poke --- spec/core/ics-002-client-semantics/README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/spec/core/ics-002-client-semantics/README.md b/spec/core/ics-002-client-semantics/README.md index 4f4479139..427562ecb 100644 --- a/spec/core/ics-002-client-semantics/README.md +++ b/spec/core/ics-002-client-semantics/README.md @@ -223,7 +223,7 @@ Client types MUST define a method to initialise a client state with a provided c type initialise = (consensusState: ConsensusState) => ClientState ``` -Client types MUST define a method to fetch the current height (height of the most recent validated header). +Client types MUST define a method to fetch the current height (height of the most recent validated state update). ```typescript type latestClientHeight = ( @@ -293,7 +293,7 @@ now-finalised consensus roots and update any necessary signature authority track changes to the validator set) for future calls to the validity predicate. Clients MAY have time-sensitive validity predicates, such that if no ClientMessage is provided for a period of time -(e.g. an unbonding period of three weeks) it will no longer be possible to update the client. +(e.g. an unbonding period of three weeks) it will no longer be possible to update the client, i.e., the client is being frozen. In this case, a permissioned entity such as a chain governance system or trusted multi-signature MAY be allowed to intervene to unfreeze a frozen client & provide a new correct ClientMessage. @@ -578,18 +578,17 @@ function updateClient( if foundMisbehaviour { clientState.UpdateStateOnMisbehaviour(header) // emit misbehaviour event - return } - - clientState.UpdateState(clientMessage) // expects no-op on duplicate header - // emit update event - return + else { + clientState.UpdateState(clientMessage) // expects no-op on duplicate header + // emit update event + } } ``` #### Misbehaviour -If a relayer wishes to explicitly submit misbehaviour. They may alert the client to the misbehaviour directly, possibly invalidating +A relayer may alert the client to the misbehaviour directly, possibly invalidating previously valid state roots & preventing future updates. ```typescript From 99554dc0e03229837e144d870957ee251f9ed717 Mon Sep 17 00:00:00 2001 From: Aditya Sripal Date: Wed, 17 Aug 2022 15:35:00 +0200 Subject: [PATCH 06/10] remove non-example instances of Header --- spec/core/ics-002-client-semantics/README.md | 208 ++----------------- 1 file changed, 12 insertions(+), 196 deletions(-) diff --git a/spec/core/ics-002-client-semantics/README.md b/spec/core/ics-002-client-semantics/README.md index 427562ecb..8b0b59622 100644 --- a/spec/core/ics-002-client-semantics/README.md +++ b/spec/core/ics-002-client-semantics/README.md @@ -251,9 +251,9 @@ type ClientMessage = bytes #### Validity predicate -A validity predicate is an opaque function defined by a client type to verify `Header`s depending on the current `ConsensusState`. +A validity predicate is an opaque function defined by a client type to verify `ClientMessage`s depending on the current `ConsensusState`. Using the validity predicate SHOULD be far more computationally efficient than replaying the full consensus algorithm -for the given parent `Header` and the list of network messages. +for the given parent `ClientMessage` and the list of network messages. The validity predicate is defined as: @@ -266,7 +266,7 @@ type VerifyClientMessage = (ClientMessage) => Void #### Misbehaviour predicate A misbehaviour predicate is an opaque function defined by a client type, used to check if a ClientMessage -constitutes a violation of the consensus protocol. This might be two signed headers +constitutes a violation of the consensus protocol. For example, if the state machine is a blockchain; this might be two signed headers with different state roots but the same height, a signed header containing invalid state transitions, or other proof of malfeasance as defined by the consensus algorithm. @@ -325,8 +325,8 @@ at a particular finalised height (necessarily associated with a particular commi 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). -- The `delayPeriodTime` is passed to the verification functions for packet-related proofs in order to allow packets to specify a period of time which must pass after a header is verified before the packet is allowed to be processed. -- The `delayPeriodBlocks` is passed to the verification functions for packet-related proofs in order to allow packets to specify a period of blocks which must pass after a header is verified before the packet is allowed to be processed. +- The `delayPeriodTime` is passed to the verification functions for packet-related proofs in order to allow packets to specify a period of time which must pass after a consensus state is added before it can be used for packet-related verification. +- The `delayPeriodBlocks` is passed to the verification functions for packet-related proofs in order to allow packets to specify a period of blocks which must pass after a consensus state is added before it can be used for packet-related verification. ##### Required functions @@ -371,10 +371,10 @@ type verifyNonMembership = ( These query endpoints are assumed to be exposed over HTTP or an equivalent RPC API by nodes of the chain associated with a particular client. -`queryHeader` MUST be defined by the chain which is validated by a particular client, and should allow for retrieval of headers by height. This endpoint is assumed to be untrusted. +`queryUpdate` MUST be defined by the chain which is validated by a particular client, and should allow for retrieval of clientMessage for a given height. This endpoint is assumed to be untrusted. ```typescript -type queryHeader = (height: Height) => Header +type queryUpdate = (height: Height) => ClientMessage ``` `queryChainConsensusState` MAY be defined by the chain which is validated by a particular client, to allow for the retrieval of the current consensus state which can be used to construct a new client. @@ -551,8 +551,8 @@ the specific paths which must be queried are defined by each client type. #### Update -Updating a client is done by submitting a new `Header`. The `Identifier` is used to point to the -stored `ClientState` that the logic will update. When a new `Header` is verified with +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 with the stored `ClientState`'s validity predicate and `ConsensusState`, the client MUST update its internal state accordingly, possibly finalising commitment roots and updating the signature authority logic in the stored consensus state. @@ -576,11 +576,11 @@ function updateClient( foundMisbehaviour := clientState.CheckForMisbehaviour(clientMessage) if foundMisbehaviour { - clientState.UpdateStateOnMisbehaviour(header) + clientState.UpdateStateOnMisbehaviour(clientMessage) // emit misbehaviour event } else { - clientState.UpdateState(clientMessage) // expects no-op on duplicate header + clientState.UpdateState(clientMessage) // expects no-op on duplicate clientMessage // emit update event } } @@ -610,191 +610,7 @@ function submitMisbehaviourToClient( ### Example Implementation -An example validity predicate is constructed for a chain running a single-operator consensus algorithm, -where the valid blocks are signed by the operator. The operator signing Key -can be changed while the chain is running. - -The client-specific types are then defined as follows: - -- `ConsensusState` stores the latest height and latest public key -- `ClientMessage` for UpdateState contain a height, a new commitment root, a signature by the operator, and possibly a new public key -- `ClientMessage` for UpdateStateForMisbehaviour contains two conflicting commitment roots signed by the same operator at the same height -- `verifyClientMessage` verifies that the operator did sign the given commitment root at the given height -- `checkForMisbehaviour` verifies that the two commitment roots in ClientMessage are different for the same height -- `checkValidityAndUpdateState` checks that the submitted height is monotonically increasing, then mutates the internal state -- `checkMisbehaviourAndUpdateState` mutates the internal state for misbehaviour - -```typescript -type Height = uint64 - -function compare(h1: Height, h2: Height): Ord { - if h1 < h2 - return LT - else if h1 === h2 - return EQ - else - return GT -} - -// information on when a header was received by the client -// this will be used to verify delay period has passed -// during proof verification -interface MessageReceiptInfo{ - height: Height - time: uint64 -} - -interface ClientState { - frozen: boolean - pastPublicKeys: Set - verifiedRoots: Map - messageReceipts: Map -} - -interface ConsensusState { - sequence: uint64 - publicKey: PublicKey -} - -interface Header { - sequence: uint64 - commitmentRoot: CommitmentRoot - signature: Signature - newPublicKey: Maybe -} - -interface ClientMessage { - headers: List
-} - -// algorithm run by operator to commit a new block -function commit( - commitmentRoot: CommitmentRoot, - sequence: uint64, - newPublicKey: Maybe): Header { - signature = privateKey.sign(commitmentRoot, sequence, newPublicKey) - header = {sequence, commitmentRoot, signature, newPublicKey} - return header -} - -// initialisation function defined by the client type -function initialise(consensusState: ConsensusState): () { - clientState = { - frozen: false, - pastPublicKeys: Set.singleton(consensusState.publicKey), - verifiedRoots: Map.empty() - } - provableStore.set(identifier, clientState) -} - -// validity predicate function defined by the client type -function verifyClientMessage( - clientState: ClientState, - clientMessage: ClientMessage, -) { - for h in clientMessage.headers { - abortTransactionUnless(consensusState.publicKey.verify(h.signature)) - } -} - -// misbehaviour predicate function defined by the client type -function checkForMisbehaviour( - clientState: ClientState, - clientMessage: ClientMessage, -) bool { - if len(clientMessage.Headers != 2) { - return false - } - // headers must be at same height - if clientMessage.headers[0].sequence != clientMessage.headers[1].sequence { - return false - } - // headers must have different roots - if clientMessage.headers[1].commitmentRoot == clientMessage.headers[1].commitmentRoot { - return false - } - return true -} - -// updateState function defined by the client type -function updateState( - clientState: ClientState, - clientMessage: ClientMessage) { - // supports 1 update at a time - abortTransactionUnless(len(clientMessage.headers) = 1) - header = clientMessage.headers[0] - abortTransactionUnless(consensusState.sequence + 1 === header.sequence) - if (header.newPublicKey !== null) { - consensusState.publicKey = header.newPublicKey - clientState.pastPublicKeys.add(header.newPublicKey) - } - consensusState.sequence = header.sequence - clientState.verifiedRoots[sequence] = header.commitmentRoot - // store the height and time at which the client received the header - // so we can ensure that delay period has passed before we use this header for proof verification - messageReceipt = MessageReceiptInfo{ - height: currentHeight(), - time: currentTimestamp() - } - clientState.messageReceipts[sequence] = messageReceipt -} - -// updateStateOnMisbehaviour function defined by the client type -function updateStateOnMisbehaviour( - clientState: ClientState, - clientMessage: ClientMessage) { - clientState.frozen = true -} - -// verifyMembership function defined by the client type -function verifyMembership( - clientState: ClientState, - height: Height, - delayPeriodTime: uint64, - delayPeriodBlocks: uint64, - proof: CommitmentProof, - path: CommitmentPath, - value: bytes) => boolean { - abortTransactionUnless(!clientState.frozen) - if delayPeriodBlocks != 0 { - // ensure that `delayPeriodBlocks` has passed since receiving the header for this height - // before using in proof verification - receivedHeight = clientState.messageReceipts[height].height - abortTransactionUnless(currentHeight() - receivedHeight >= delayPeriodBlocks) - } - if delayPeriodTime != 0 { - // ensure that `delayPeriodTime` has passed since receiving the header for this height - // before using in proof verification - receivedTime = clientState.messageReceipts[height].time - abortTransactionUnless(currentTimestamp() - receivedTime >= delayPeriodTime) - } - return clientState.verifiedRoots[height].verifyMembership(path, value, proof) -} - -// verifyNonMembership function defined by the client type -function verifyNonMembership( - clientState: ClientState, - height: Height, - prefix: CommitmentPrefix, - delayPeriodTime: uint64, - delayPeriodBlocks: uint64, - proof: CommitmentProof, - path) => boolean { - abortTransactionUnless(!clientState.frozen) - if delayPeriodBlocks != 0 { - // ensure that `delayPeriodBlocks` has passed since receiving the header for this height - // before using in proof verification - receivedHeight = clientState.messageReceipts[height].height - abortTransactionUnless(currentHeight() - receivedHeight >= delayPeriodBlocks) - } - if delayPeriodTime != 0 { - // ensure that `delayPeriodTime` has passed since receiving the header for this height - // before using in proof verification - receivedTime = clientState.messageReceipts[height].time - abortTransactionUnless(currentTimestamp() - receivedTime >= delayPeriodTime) - } - return clientState.verifiedRoots[height].verifyNonMembership(path, proof) -} +Please see the ibc-go implementations of light clients for examples of how to implement your own: https://github.com/cosmos/ibc-go/blob/main/modules/light-clients ### Properties & Invariants From 61a1a9c4642c3e8f37feb0bb66b420f16670a605 Mon Sep 17 00:00:00 2001 From: Aditya Sripal Date: Wed, 17 Aug 2022 18:05:26 +0200 Subject: [PATCH 07/10] clientmsg definition --- spec/core/ics-002-client-semantics/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/core/ics-002-client-semantics/README.md b/spec/core/ics-002-client-semantics/README.md index 8b0b59622..03c8a0ac6 100644 --- a/spec/core/ics-002-client-semantics/README.md +++ b/spec/core/ics-002-client-semantics/README.md @@ -100,7 +100,11 @@ types may require additional properties. it enables proofs of inclusion or non-inclusion of particular values at particular paths in the state of the remote state machine at particular `Height`s. -* `ValidityPredicate` is a function that validates state updates. +* `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`. * `ConsensusState` is the *trusted view* of the state of a state machine at a particular `Height`. From 360a26d06be6474b5a24792adc5f9f6e0875f71b Mon Sep 17 00:00:00 2001 From: Aditya Sripal Date: Wed, 17 Aug 2022 18:33:17 +0200 Subject: [PATCH 08/10] fix updateClient pseudocode --- spec/core/ics-002-client-semantics/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/core/ics-002-client-semantics/README.md b/spec/core/ics-002-client-semantics/README.md index 03c8a0ac6..23f70b900 100644 --- a/spec/core/ics-002-client-semantics/README.md +++ b/spec/core/ics-002-client-semantics/README.md @@ -573,10 +573,9 @@ to allow governance mechanisms to perform these actions ```typescript function updateClient( id: Identifier, + clientState: ClientState, clientMessage: ClientMessage) { - if err := clientState.VerifyClientMessage(clientMessage); err != nil { - return err - } + clientState.VerifyClientMessage(clientMessage) foundMisbehaviour := clientState.CheckForMisbehaviour(clientMessage) if foundMisbehaviour { From c70ae5ba58360a802dd59e1d5a117dc7116ca05e Mon Sep 17 00:00:00 2001 From: Aditya Sripal Date: Wed, 24 Aug 2022 16:43:34 +0200 Subject: [PATCH 09/10] address marius reviews --- spec/core/ics-002-client-semantics/README.md | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/spec/core/ics-002-client-semantics/README.md b/spec/core/ics-002-client-semantics/README.md index 23f70b900..4725e16ed 100644 --- a/spec/core/ics-002-client-semantics/README.md +++ b/spec/core/ics-002-client-semantics/README.md @@ -270,7 +270,7 @@ type VerifyClientMessage = (ClientMessage) => Void #### Misbehaviour predicate A misbehaviour predicate is an opaque function defined by a client type, used to check if a ClientMessage -constitutes a violation of the consensus protocol. For example, if the state machine is a blockchain; this might be two signed headers +constitutes a violation of the consensus protocol. For example, if the state machine is a blockchain, this might be two signed headers with different state roots but the same height, a signed header containing invalid state transitions, or other proof of malfeasance as defined by the consensus algorithm. @@ -332,8 +332,6 @@ Internal implementation details may differ (for example, a loopback client could - The `delayPeriodTime` is passed to the verification functions for packet-related proofs in order to allow packets to specify a period of time which must pass after a consensus state is added before it can be used for packet-related verification. - The `delayPeriodBlocks` is passed to the verification functions for packet-related proofs in order to allow packets to specify a period of blocks which must pass after a consensus state is added before it can be used for packet-related verification. -##### Required functions - `verifyMembership` is a generic proof verification method which verifies a proof of the existence of a value at a given `CommitmentPath` at the specified height. The caller is expected to construct the full `CommitmentPath` from a `CommitmentPrefix` and a standardized path (as defined in [ICS 24](../ics-024-host-requirements/README.md#path-space)). If the caller desires a particular delay period to be enforced, then it can pass in a non-zero `delayPeriodTime` or `delayPeriodBlocks`. If a delay period is not necessary, the caller must pass in 0 for `delayPeriodTime` and `delayPeriodBlocks`, @@ -573,8 +571,11 @@ to allow governance mechanisms to perform these actions ```typescript function updateClient( id: Identifier, - clientState: ClientState, clientMessage: ClientMessage) { + // get clientState from store with id + clientState = provableStore.get(clientStatePath(id)) + abortTransactionUnless(clientState !== null) + clientState.VerifyClientMessage(clientMessage) foundMisbehaviour := clientState.CheckForMisbehaviour(clientMessage) @@ -598,23 +599,17 @@ previously valid state roots & preventing future updates. function submitMisbehaviourToClient( id: Identifier, clientMessage: ClientMessage) { - clientType = provableStore.get(clientTypePath(id)) - abortTransactionUnless(clientType !== null) clientState = provableStore.get(clientStatePath(id)) abortTransactionUnless(clientState !== null) // authenticate client message clientState.verifyClientMessage(clientMessage) // check that client message is valid instance of misbehaviour - clientState.checkForMisbehaviour(clientMessage) + abortTransactionUnless(clientState.checkForMisbehaviour(clientMessage)) // update state based on misbehaviour clientState.UpdateStateOnMisbehaviour(misbehaviour) } ``` -### Example Implementation - -Please see the ibc-go implementations of light clients for examples of how to implement your own: https://github.com/cosmos/ibc-go/blob/main/modules/light-clients - ### 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). @@ -629,7 +624,7 @@ New client types can be added by IBC implementations at-will as long as they con ## Example Implementation -Coming soon. +Please see the ibc-go implementations of light clients for examples of how to implement your own: https://github.com/cosmos/ibc-go/blob/main/modules/light-clients ## Other Implementations From b6f40485d6013a721af7e91e3b44584afabe87a3 Mon Sep 17 00:00:00 2001 From: Aditya Sripal Date: Wed, 24 Aug 2022 16:45:30 +0200 Subject: [PATCH 10/10] fix headings --- spec/core/ics-002-client-semantics/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/core/ics-002-client-semantics/README.md b/spec/core/ics-002-client-semantics/README.md index 4725e16ed..d1b4b9017 100644 --- a/spec/core/ics-002-client-semantics/README.md +++ b/spec/core/ics-002-client-semantics/README.md @@ -324,7 +324,7 @@ to intervene to unfreeze a frozen client & provide a new correct ClientMessage w 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 +### 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). @@ -367,7 +367,7 @@ type verifyNonMembership = ( => boolean ``` -#### Query interface +### Query interface ##### Chain queries