From 33e046006e57f99caf1cfd57c23d30b498373d27 Mon Sep 17 00:00:00 2001 From: Rivers Yang Date: Thu, 12 Jan 2023 18:13:21 +0800 Subject: [PATCH 1/5] Add spec of ics 012. --- spec/client/ics-012-near-client/README.md | 459 ++++++++++++++++++++++ 1 file changed, 459 insertions(+) create mode 100644 spec/client/ics-012-near-client/README.md diff --git a/spec/client/ics-012-near-client/README.md b/spec/client/ics-012-near-client/README.md new file mode 100644 index 000000000..168963265 --- /dev/null +++ b/spec/client/ics-012-near-client/README.md @@ -0,0 +1,459 @@ +--- +ics: 12 +title: NEAR Client +stage: draft +category: IBC/TAO +kind: instantiation +implements: - +author: Rivers Yang +created: 2023-1-12 +modified: - +--- + +## Synopsis + +This specification document describes a client (verification algorithm) for NEAR protocol. + +### Motivation + +State machine of NEAR protocol 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). + +`hash` is a generic collision-resistant hash function. In NEAR protocol, it is `sha2::sha256` hash function. We define a new type for the result of hash function as: + +```typescript +type CryptoHash = [32]byte +``` + +We also defines the types for public key and signature. In NEAR protocol, they are based on `ed25519` signature algorithm: + +```typescript +type PublicKey = ED25519PublicKey +type Signature = ED25519Signature +``` + +`borsh` is a generic serialization function which follows the [Borsh serialization format](https://borsh.io/). + +`merklize` is a generic function which can construct a merkle tree from an array which the element in it can be serialized by `borsh`. This function should return the merkle root of the tree at least. + +In NEAR protocol, the block producers are changed by time (about 12 hours). The period is known as `epoch` and the id of a epoch is represented by a `CryptoHash`. + +### Desired Properties + +This specification must satisfy the client interface defined in ICS 2. + +## Technical Specification + +This specification is based on the [NEAR light client specification](https://nomicon.io/ChainSpec/LightClient) and the implementation of [nearcore v1.30.0](https://github.com/near/nearcore/releases/tag/1.30.0) by adding necessary data fields and checking processes. + +### Client state + +The NEAR client state tracks the latest height and cached heights. + +```typescript +interface ClientState { + trustingPeriod: uint64 + latestHeight: Height + latestTimestamp: uint64 + upgradeCommitmentPrefix: []byte + upgradeKey: []bype +} +``` + +### Consensus state + +The NEAR client tracks the block producers of current epoch and header (refer to [Headers section](#headers)) for all previously verified consensus states (these can be pruned after the unbonding period has passed, but should not be pruned beforehand). + +```typescript +interface ValidatorStakeView { + accountId: String + publicKey: PublicKey + stake: uint128 +} + +interface ConsensusState { + currentBps: List + header: Header +} +``` + +### Height + +The height of a NEAR client is an `uint64` number. + +```typescript +interface Height { + height: uint64 +} +``` + +Comparison between heights is implemented as follows: + +```typescript +function compare(a: Height, b: Height): Ord { + if (a.height < b.height) + return LT + else if (a.height === b.height) + return EQ + return GT +} +``` + +### Headers + +The NEAR client headers include the `LightClientBlockView` and previou state root of chunks. The entire block producers for next epoch and approvals for the block after the next are included in `LightClientBlockView`. + +```typescript +interface BlockHeaderInnerLiteView { + height: Height + epochId: CryptoHash + nextEpochId: CryptoHash + prevStateRoot: CryptoHash + outcomeRoot: CryptoHash + timestampNanosec: uint64 + nextBpHash: CryptoHash + blockMerkleRoot: CryptoHash +} + +interface LightClientBlockView { + prevBlockHash: CryptoHash + nextBlockInnerHash: CryptoHash, + innerLite: BlockHeaderInnerLiteView + innerRestHash: CryptoHash + nextBps: Maybe> + approvalsAfterNext: List> +} + +interface Header { + lightClientBlockView: LightClientBlockView + prevStateRootOfChunks: List +} +``` + +The current block hash, next block hash and approval message can be calcuated from `LightClientBlockView`. + +```typescript +function (LightClientBlockView) currentBlockHash(): CryptoHash { + return hash(concat( + hash(concat( + hash(borsh(self.innerLite)), + self.innerRestHash, + )), + self.prevBlockHash + )) +} + +function (LightClientBlockView) nextBlockHash(): CryptoHash { + return hash( + concat(self.nextBlockInnerHash, + self.currentBlockHash() + )) +} + +enum ApprovalInner { + Endorsement(CryptoHash), + Skip(uint64) +} + +function (LightClientBlockView) approvalMessage(): []byte { + return concat( + borsh(ApprovalInner::Endorsement(self.nextBlockHash())), + littleEndian(self.innerLite.height + 2) + ) +} +``` + +Header implements `ClientMessage` interface. + +### Misbehaviour + +TBD (currently not applicable in NEAR protocol) + +### Client initialisation + +The NEAR client initialisation requires a latest consensus state (the latest header and the block producers of the epoch of the header). + +```typescript +function initialise( + trustingPeriod: uint64, + latestTimestamp: uint64, + upgradeCommitmentPrefix: []byte, + upgradeKey: []bype, + consensusState: ConsensusState): ClientState { + assert(consensusState.currentBps.len > 0) + assert(consensusState.header.getHeight() > 0) + // implementations may define a identifier generation function + identifier = generateClientIdentifier() + set("clients/{identifier}/consensusStates/{consensusState.header.getHeight()}", consensusState) + height = Height { + height: consensusState.header.getHeight() + } + return ClientState { + trustingPeriod + latestHeight: height + latestTimestamp + upgradeCommitmentPrefix + upgradeKey + } +} +``` + +### Validity Predicate + +The NEAR client validity checking uses spec described in the [NEAR light client specification](https://nomicon.io/ChainSpec/LightClient) by adding an extra checking for the previous state root of chunks. If the provided header is valid, the client state is updated and the newly verified header is written to the store. + +```typescript +function verifyClientMessage( + clientMsg: ClientMessage) { + switch typeof(clientMsg) { + case Header: + verifyHeader(clientMsg) + } +} +``` + +Verify validity of regular update to the NEAR client + +```typescript +function (Header) getHeight(): Height { + return self.lightClientBlockView.innerLite.height +} + +function (Header) getEpochId(): CryptoHash { + return self.lightClientBlockView.innerLite.epochId +} + +function (Header) getNextEpochId(): CryptoHash { + return self.lightClientBlockView.innerLite.nextEpochId +} + +function (ClientState) getBlockProducersOf(epochId: CryptoHash): List { + consensusState = get("clients/{clientMsg.identifier}/consensusStates/{self.latestHeight}") + if epochId === consensusState.header.getEpochId() { + return consensusState.currentBps + } else if epochId === consensusState.header.getNextEpochId() { + return consensusState.header.lightClientBlockView.nextBps + } else { + return null + } +} + +function verifyHeader(header: Header) { + clientState = get("clients/{header.identifier}/clientState") + + latestHeader = clientState.getLatestHeader() + approvalMessage = header.lightClientBlockView.approvalMessage() + + // (1) The height of the block is higher than the height of the current head. + assert(clientState.latestHeight < header.getHeight()) + + // (2) The epoch of the block is equal to the epochId or nextEpochId known for the current head. + assert(header.getEpochId() in + [latestHeader.getEpochId(), latestHeader.getNextEpochId()]) + + // (3) If the epoch of the block is equal to the nextEpochId of the head, then nextBps is not null. + assert(not(header.getEpochId() == latestHeader.getNextEpochId() + && header.lightClientBlockView.nextBps === null)) + + // (4) approvalsAfterNext contain valid signatures on approvalMessage from the block producers of the corresponding epoch + // (5) The signatures present in approvalsAfterNext correspond to more than 2/3 of the total stake + totalStake = 0 + approvedStake = 0 + + epochBlockProducers = clientState.getBlockProducersOf(header.getEpochId()) + for maybeSignature, blockProducer in zip(header.lightClientBlockView.approvalsAfterNext, epochBlockProducers) { + totalStake += blockProducer.stake + + if maybeSignature === null { + continue + } + + approvedStake += blockProducer.stake + + assert(verify_signature( + public_key: blockProducer.public_key, + signature: maybeSignature, + message: approvalMessage + )) + } + + threshold = totalStake * 2 / 3 + assert(approvedStake > threshold) + + // (6) If nextBps is not none, hash(borsh(nextBps)) corresponds to the nextBpHash in innerLite + if header.lightClientBlockView.nextBps !== null { + assert(hash(borsh(header.lightClientBlockView.nextBps)) === header.lightClientBlockView.innerLite.nextBpHash) + } + + // (7) Check the prevStateRoot is the root of merklized prevStateRootOfChunks + assert(header.lightClientBlockView.innerLite.prevStateRoot === merklize(header.prevStateRootOfChunks).root) +} +``` + +### Misbehaviour Predicate + +TBD (currently not applicable in NEAR protocol) + +### UpdateState + +UpdateState will perform a regular update for the NEAR client. It will add a consensus state to the client store. If the header is higher than the lastest height on the clientState, then the clientState will be updated. + +```typescript +function updateState(clientMessage: clientMessage) { + clientState = 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 + set("clients/{clientMsg.identifier}/clientState", clientState) + } + + currentBps = clientState.getBlockProducersOf(header.getEpochId()) + // create recorded consensus state, save it + consensusState = ConsensusState { currentBps, header } + set("clients/{clientMsg.identifier}/consensusStates/{header.getHeight()}", consensusState) +} +``` + +### UpdateStateOnMisbehaviour + +TBD (currently not applicable in NEAR protocol) + +### 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 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 > clientState.latestHeight) + // 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) + // fetch the previously verified commitment root & verify membership + root = get("clients/{clientMsg.identifier}/consensusStates/{height}") + // verify that the provided consensus state has been stored + assert(root.verifyMembership(path, newClientState, proof)) + // update client state + clientState = newClientState + set("clients/{clientMsg.identifier}/clientState", clientState) +} +``` + +### State verification functions + +The NEAR client state verification functions check a MPT (Merkle Patricia Tree) proof against a previously validated consensus state. + +```typescript +function (ConsensusState) verifyMembership( + path: []byte, + value: []byte, + proof: [][]byte): bool { + // Check that the root in proof is one of the prevStateRoot of a chunk + assert(hash(proof[0]) in self.header.prevStateRootOfChunks) + // Check the value on the path is exactly the given value with proof data + // based on MPT construction algorithm +} + +function verifyMembership( + clientState: ClientState, + height: Height, + delayTimePeriod: uint64, + delayBlockPeriod: uint64, + proof: CommitmentProof, + path: CommitmentPath, + value: []byte) { + // 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 + root = get("clients/{clientIdentifier}/consensusStates/{height}") + // verify that has been stored + assert(root.verifyMembership(path, value, proof)) +} + +function (ConsensusState) verifyNonMembership( + path: []byte, + proof: [][]byte): bool { + // Check that the root in proof is one of the prevStateRoot of a chunk + assert(hash(proof[0]) in self.header.prevStateRootOfChunks) + // Check that there is NO value on the path with proof data + // based on MPT construction algorithm +} + +function verifyNonMembership( + clientState: ClientState, + height: Height, + delayTimePeriod: uint64, + delayBlockPeriod: uint64, + proof: CommitmentProof, + path: CommitmentPath) { + // 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 + root = get("clients/{identifier}/consensusStates/{height}") + // verify that nothing has been stored at path + assert(root.verifyNonMembership(path, proof)) +} +``` + +### Properties & Invariants + +Correctness guarantees as provided by the NEAR light client algorithm. + +## Backwards Compatibility + +Not applicable. + +## Forwards Compatibility + +Not applicable. Alterations to the client verification algorithm will require a new client standard. + +## Example Implementation + +None yet. + +## Other Implementations + +None at present. + +## History + +January 12th, 2023 - Initial version + +## Copyright + +All content herein is licensed under [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0). From 012061480ea2dac474b123ad810d8a386ebd92a8 Mon Sep 17 00:00:00 2001 From: Rivers Yang Date: Thu, 12 Jan 2023 18:22:44 +0800 Subject: [PATCH 2/5] Fix format error. --- spec/client/ics-012-near-client/README.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/spec/client/ics-012-near-client/README.md b/spec/client/ics-012-near-client/README.md index 168963265..4fddc6096 100644 --- a/spec/client/ics-012-near-client/README.md +++ b/spec/client/ics-012-near-client/README.md @@ -4,10 +4,9 @@ title: NEAR Client stage: draft category: IBC/TAO kind: instantiation -implements: - +implements: 1 author: Rivers Yang created: 2023-1-12 -modified: - --- ## Synopsis @@ -94,11 +93,11 @@ Comparison between heights is implemented as follows: ```typescript function compare(a: Height, b: Height): Ord { - if (a.height < b.height) - return LT - else if (a.height === b.height) - return EQ - return GT + if (a.height < b.height) + return LT + else if (a.height === b.height) + return EQ + return GT } ``` @@ -308,11 +307,11 @@ function updateState(clientMessage: 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() + // update latest height + clientState.latestHeight = header.getHeight() - // save the client - set("clients/{clientMsg.identifier}/clientState", clientState) + // save the client + set("clients/{clientMsg.identifier}/clientState", clientState) } currentBps = clientState.getBlockProducersOf(header.getEpochId()) From f248e757bf62a5fcecc0cbdbd4856ac7a8c1a97c Mon Sep 17 00:00:00 2001 From: Rivers Yang Date: Thu, 12 Jan 2023 20:16:28 +0800 Subject: [PATCH 3/5] Fix format errors and typos. --- spec/client/ics-012-near-client/README.md | 64 +++++++++++++---------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/spec/client/ics-012-near-client/README.md b/spec/client/ics-012-near-client/README.md index 4fddc6096..2a6cfb4ec 100644 --- a/spec/client/ics-012-near-client/README.md +++ b/spec/client/ics-012-near-client/README.md @@ -4,7 +4,7 @@ title: NEAR Client stage: draft category: IBC/TAO kind: instantiation -implements: 1 +implements: none author: Rivers Yang created: 2023-1-12 --- @@ -64,7 +64,7 @@ interface ClientState { ### Consensus state -The NEAR client tracks the block producers of current epoch and header (refer to [Headers section](#headers)) for all previously verified consensus states (these can be pruned after the unbonding period has passed, but should not be pruned beforehand). +The NEAR client tracks the block producers of current epoch and header (refer to [Headers section](#headers)) for all previously verified consensus states (these can be pruned after switching to the epoch after the next, but should not be pruned beforehand). ```typescript interface ValidatorStakeView { @@ -176,6 +176,10 @@ TBD (currently not applicable in NEAR protocol) The NEAR client initialisation requires a latest consensus state (the latest header and the block producers of the epoch of the header). ```typescript +function (Header) getHeight(): Height { + return self.lightClientBlockView.innerLite.height +} + function initialise( trustingPeriod: uint64, latestTimestamp: uint64, @@ -186,10 +190,10 @@ function initialise( assert(consensusState.header.getHeight() > 0) // implementations may define a identifier generation function identifier = generateClientIdentifier() - set("clients/{identifier}/consensusStates/{consensusState.header.getHeight()}", consensusState) height = Height { height: consensusState.header.getHeight() } + set("clients/{identifier}/consensusStates/{height}", consensusState) return ClientState { trustingPeriod latestHeight: height @@ -205,11 +209,10 @@ function initialise( The NEAR client validity checking uses spec described in the [NEAR light client specification](https://nomicon.io/ChainSpec/LightClient) by adding an extra checking for the previous state root of chunks. If the provided header is valid, the client state is updated and the newly verified header is written to the store. ```typescript -function verifyClientMessage( - clientMsg: ClientMessage) { - switch typeof(clientMsg) { +function verifyClientMessage(clientMessage: ClientMessage) { + switch typeof(clientMessage) { case Header: - verifyHeader(clientMsg) + verifyHeader(clientMessage) } } ``` @@ -217,10 +220,6 @@ function verifyClientMessage( Verify validity of regular update to the NEAR client ```typescript -function (Header) getHeight(): Height { - return self.lightClientBlockView.innerLite.height -} - function (Header) getEpochId(): CryptoHash { return self.lightClientBlockView.innerLite.epochId } @@ -230,7 +229,7 @@ function (Header) getNextEpochId(): CryptoHash { } function (ClientState) getBlockProducersOf(epochId: CryptoHash): List { - consensusState = get("clients/{clientMsg.identifier}/consensusStates/{self.latestHeight}") + consensusState = get("clients/{clientMessage.identifier}/consensusStates/{self.latestHeight}") if epochId === consensusState.header.getEpochId() { return consensusState.currentBps } else if epochId === consensusState.header.getNextEpochId() { @@ -241,7 +240,7 @@ function (ClientState) getBlockProducersOf(epochId: CryptoHash): List= height) // fetch the previously verified commitment root & verify membership - root = get("clients/{clientMsg.identifier}/consensusStates/{height}") + root = get("clients/{clientMessage.identifier}/consensusStates/{height}") // verify that the provided consensus state has been stored assert(root.verifyMembership(path, newClientState, proof)) // update client state clientState = newClientState - set("clients/{clientMsg.identifier}/clientState", clientState) + set("clients/{clientMessage.identifier}/clientState", clientState) } ``` @@ -390,9 +396,9 @@ function verifyMembership( // 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 - root = get("clients/{clientIdentifier}/consensusStates/{height}") + header = get("clients/{clientMessage.identifier}/consensusStates/{height}") // verify that has been stored - assert(root.verifyMembership(path, value, proof)) + assert(header.verifyMembership(path, value, proof)) } function (ConsensusState) verifyNonMembership( @@ -423,9 +429,9 @@ function verifyNonMembership( // 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 - root = get("clients/{identifier}/consensusStates/{height}") + header = get("clients/{clientMessage.identifier}/consensusStates/{height}") // verify that nothing has been stored at path - assert(root.verifyNonMembership(path, proof)) + assert(header.verifyNonMembership(path, proof)) } ``` From 66303234247ecfedd164eea94e1ccb0598edaa17 Mon Sep 17 00:00:00 2001 From: Rivers Yang Date: Thu, 19 Jan 2023 16:27:38 +0800 Subject: [PATCH 4/5] Add spec for misbehaviour related sections. --- spec/client/ics-012-near-client/README.md | 168 ++++++++++++++++------ 1 file changed, 127 insertions(+), 41 deletions(-) diff --git a/spec/client/ics-012-near-client/README.md b/spec/client/ics-012-near-client/README.md index 2a6cfb4ec..3ae06d510 100644 --- a/spec/client/ics-012-near-client/README.md +++ b/spec/client/ics-012-near-client/README.md @@ -38,7 +38,7 @@ type Signature = ED25519Signature `merklize` is a generic function which can construct a merkle tree from an array which the element in it can be serialized by `borsh`. This function should return the merkle root of the tree at least. -In NEAR protocol, the block producers are changed by time (about 12 hours). The period is known as `epoch` and the id of a epoch is represented by a `CryptoHash`. +In NEAR protocol, the block producers are changed by time (about 12 hours). The period is known as `epoch` and the id of an epoch is represented by a `CryptoHash`. ### Desired Properties @@ -50,13 +50,14 @@ This specification is based on the [NEAR light client specification](https://nom ### Client state -The NEAR client state tracks the latest height and cached heights. +The NEAR client state tracks the following data: ```typescript interface ClientState { trustingPeriod: uint64 latestHeight: Height latestTimestamp: uint64 + frozenHeight: Maybe upgradeCommitmentPrefix: []byte upgradeKey: []bype } @@ -64,7 +65,7 @@ interface ClientState { ### Consensus state -The NEAR client tracks the block producers of current epoch and header (refer to [Headers section](#headers)) for all previously verified consensus states (these can be pruned after switching to the epoch after the next, but should not be pruned beforehand). +The NEAR client tracks the block producers of current epoch and header (refer to [Headers section](#headers)) for all previously verified consensus states (these can be pruned after a certain period, but should not be pruned beforehand). ```typescript interface ValidatorStakeView { @@ -103,39 +104,39 @@ function compare(a: Height, b: Height): Ord { ### Headers -The NEAR client headers include the `LightClientBlockView` and previou state root of chunks. The entire block producers for next epoch and approvals for the block after the next are included in `LightClientBlockView`. +The NEAR client headers include the `LightClientBlock` and previou state root of chunks. The entire block producers for next epoch and approvals for the block after the next are included in `LightClientBlock`. ```typescript -interface BlockHeaderInnerLiteView { +interface BlockHeaderInnerLite { height: Height epochId: CryptoHash nextEpochId: CryptoHash prevStateRoot: CryptoHash outcomeRoot: CryptoHash - timestampNanosec: uint64 + timestamp: uint64 // in nanoseconds nextBpHash: CryptoHash blockMerkleRoot: CryptoHash } -interface LightClientBlockView { +interface LightClientBlock { prevBlockHash: CryptoHash nextBlockInnerHash: CryptoHash, - innerLite: BlockHeaderInnerLiteView + innerLite: BlockHeaderInnerLite innerRestHash: CryptoHash nextBps: Maybe> approvalsAfterNext: List> } interface Header { - lightClientBlockView: LightClientBlockView + lightClientBlock: LightClientBlock prevStateRootOfChunks: List } ``` -The current block hash, next block hash and approval message can be calcuated from `LightClientBlockView`. +The current block hash, next block hash and approval message can be calcuated from `LightClientBlock`. The signatures in `approvalsAfterNext` are provided by current block producers by signing the approval message. ```typescript -function (LightClientBlockView) currentBlockHash(): CryptoHash { +function (LightClientBlock) currentBlockHash(): CryptoHash { return hash(concat( hash(concat( hash(borsh(self.innerLite)), @@ -145,7 +146,7 @@ function (LightClientBlockView) currentBlockHash(): CryptoHash { )) } -function (LightClientBlockView) nextBlockHash(): CryptoHash { +function (LightClientBlock) nextBlockHash(): CryptoHash { return hash( concat(self.nextBlockInnerHash, self.currentBlockHash() @@ -157,7 +158,7 @@ enum ApprovalInner { Skip(uint64) } -function (LightClientBlockView) approvalMessage(): []byte { +function (LightClientBlock) approvalMessage(): []byte { return concat( borsh(ApprovalInner::Endorsement(self.nextBlockHash())), littleEndian(self.innerLite.height + 2) @@ -165,11 +166,30 @@ function (LightClientBlockView) approvalMessage(): []byte { } ``` +We also defines the `CommitmentRoot` of `Header` as: + +```typescript +function (Header) commitmentRoot(): CryptoHash { + return self.lightClientBlock.innerLite.prevStateRoot +} +``` + Header implements `ClientMessage` interface. ### Misbehaviour -TBD (currently not applicable in NEAR protocol) +The `Misbehaviour` type is used for detecting misbehaviour and freezing the client - to prevent further packet flow - if applicable. +The NEAR 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 + header1: Header + header2: Header +} +``` + +> As the slashing policy is NOT applicable in NEAR protocol for now, this section is only for references. ### Client initialisation @@ -177,7 +197,7 @@ The NEAR client initialisation requires a latest consensus state (the latest hea ```typescript function (Header) getHeight(): Height { - return self.lightClientBlockView.innerLite.height + return self.lightClientBlock.innerLite.height } function initialise( @@ -193,11 +213,12 @@ function initialise( height = Height { height: consensusState.header.getHeight() } - set("clients/{identifier}/consensusStates/{height}", consensusState) + set("clients/{identifier}/consensusStates/{consensusState.header.getHeight()}", consensusState) return ClientState { trustingPeriod latestHeight: height latestTimestamp + frozenHeight: null upgradeCommitmentPrefix upgradeKey } @@ -221,19 +242,18 @@ Verify validity of regular update to the NEAR client ```typescript function (Header) getEpochId(): CryptoHash { - return self.lightClientBlockView.innerLite.epochId + return self.lightClientBlock.innerLite.epochId } function (Header) getNextEpochId(): CryptoHash { - return self.lightClientBlockView.innerLite.nextEpochId + return self.lightClientBlock.innerLite.nextEpochId } -function (ClientState) getBlockProducersOf(epochId: CryptoHash): List { - consensusState = get("clients/{clientMessage.identifier}/consensusStates/{self.latestHeight}") - if epochId === consensusState.header.getEpochId() { - return consensusState.currentBps - } else if epochId === consensusState.header.getNextEpochId() { - return consensusState.header.lightClientBlockView.nextBps +function (ConsensusState) getBlockProducersOf(epochId: CryptoHash): List { + if epochId === self.header.getEpochId() { + return self.currentBps + } else if epochId === self.header.getNextEpochId() { + return self.header.lightClientBlock.nextBps } else { return null } @@ -243,7 +263,7 @@ function verifyHeader(header: Header) { clientState = get("clients/{clientMessage.identifier}/clientState") latestHeader = clientState.getLatestHeader() - approvalMessage = header.lightClientBlockView.approvalMessage() + approvalMessage = header.lightClientBlock.approvalMessage() // (1) The height of the block is higher than the height of the current head. assert(clientState.latestHeight < header.getHeight()) @@ -256,7 +276,7 @@ function verifyHeader(header: Header) { // (3) If the epoch of the block is equal to the nextEpochId of the head, // then nextBps is not null. assert(not(header.getEpochId() == latestHeader.getNextEpochId() - && header.lightClientBlockView.nextBps === null)) + && header.lightClientBlock.nextBps === null)) // (4) approvalsAfterNext contain valid signatures on approvalMessage // from the block producers of the corresponding epoch. @@ -267,7 +287,7 @@ function verifyHeader(header: Header) { epochBlockProducers = clientState.getBlockProducersOf(header.getEpochId()) for maybeSignature, blockProducer in - zip(header.lightClientBlockView.approvalsAfterNext, epochBlockProducers) { + zip(header.lightClientBlock.approvalsAfterNext, epochBlockProducers) { totalStake += blockProducer.stake if maybeSignature === null { @@ -283,24 +303,69 @@ function verifyHeader(header: Header) { )) } - threshold = totalStake * 2 / 3 - assert(approvedStake > threshold) + assert(approvedStake * 3 > totalStake * 2) // (6) If nextBps is not none, hash(borsh(nextBps)) corresponds to the nextBpHash in innerLite - if header.lightClientBlockView.nextBps !== null { - assert(hash(borsh(header.lightClientBlockView.nextBps)) - === header.lightClientBlockView.innerLite.nextBpHash) + if header.lightClientBlock.nextBps !== null { + assert(hash(borsh(header.lightClientBlock.nextBps)) + === header.lightClientBlock.innerLite.nextBpHash) } // (7) Check the prevStateRoot is the root of merklized prevStateRootOfChunks - assert(header.lightClientBlockView.innerLite.prevStateRoot + assert(header.lightClientBlock.innerLite.prevStateRoot === merklize(header.prevStateRootOfChunks).root) } ``` ### Misbehaviour Predicate -TBD (currently not applicable in NEAR protocol) +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 (Header) timestamp(): uint64 { + return self.lightClientBlock.innerLite.timestamp +} + +function (ConsensusState) timestamp(): uint64 { + return self.header.lightClientBlock.innerLite.timestamp +} + +function checkForMisbehaviour( + clientMsg: clientMessage) => bool { + clientState = get("clients/{clientMsg.identifier}/clientState") + switch typeof(clientMsg) { + case Header: + // fetch consensusstate at header height if it exists + consensusState = 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.header.commitmentRoot() != header.commitmentRoot() { + 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: + // assert that the heights are the same + assert(misbehaviour.header1.getHeight() === misbehaviour.header2.getHeight()) + // assert that the commitments are different + assert(misbehaviour.header1.commitmentRoot() !== misbehaviour.header2.commitmentRoot()) + } +} +``` + +> As the slashing policy is NOT applicable in NEAR protocol for now, this section is only for references. ### UpdateState @@ -309,10 +374,12 @@ UpdateState will perform a regular update for the NEAR client. It will add a con ```typescript function updateState(clientMessage: clientMessage) { clientState = get("clients/{clientMessage.identifier}/clientState") - header = Header(clientMessage) + consensusState = get("clients/{clientMessage.identifier}/consensusStates/{clientState.latestHeight}") + + header = clientMessage.getHeader() // only update the clientstate if the header height is higher // than clientState latest height - if clientState.height < header.getHeight() { + if clientState.latestHeight < header.getHeight() { // update latest height clientState.latestHeight = header.getHeight() @@ -320,16 +387,35 @@ function updateState(clientMessage: clientMessage) { set("clients/{clientMessage.identifier}/clientState", clientState) } - currentBps = clientState.getBlockProducersOf(header.getEpochId()) + currentBps = consensusState.getBlockProducersOf(header.getEpochId()) // create recorded consensus state, save it - consensusState = ConsensusState { currentBps, header } - set("clients/{clientMessage.identifier}/consensusStates/{header.getHeight()}", consensusState) + newConsensusState = ConsensusState { currentBps, header } + set("clients/{clientMessage.identifier}/consensusStates/{header.getHeight()}", newConsensusState) } ``` ### UpdateStateOnMisbehaviour -TBD (currently not applicable in NEAR protocol) +UpdateStateOnMisbehaviour will set the frozen height to a non-zero sentinel height to freeze the entire client. + +```typescript +function updateStateOnMisbehaviour(clientMsg: clientMessage) { + clientState = get("clients/{clientMsg.identifier}/clientState") + if checkForMisbehaviour(clientMsg) === true { + switch typeof(clientMsg) { + case Header: + prevConsState = getPreviousConsensusState(header.getHeight()) + clientState.frozenHeight = prevConsState.header.getHeight() + case Misbehaviour: + prevConsState = getPreviousConsensusState(clientMessage.header1.getHeight()) + clientState.frozenHeight = prevConsState.header.getHeight() + } + } + set("clients/{clientMsg.identifier}/clientState", clientState) +} +``` + +> As the slashing policy is NOT applicable in NEAR protocol for now, this section is only for references. ### Upgrades @@ -363,7 +449,7 @@ function upgradeClientState( ### State verification functions -The NEAR client state verification functions check a MPT (Merkle Patricia Tree) proof against a previously validated consensus state. +The NEAR client state verification functions check a MPT (Merkle Patricia Tree) proof against a previously validated consensus state. The client should provide both membership verification and non-membership verification. ```typescript function (ConsensusState) verifyMembership( From 163c0807fbdf19c3bfb9ab126956ded4ff068ba9 Mon Sep 17 00:00:00 2001 From: Rivers Yang Date: Thu, 16 Mar 2023 17:29:59 +0800 Subject: [PATCH 5/5] Update `ics-012` spec based on first review. --- spec/client/ics-012-near-client/README.md | 141 ++++++++++++---------- 1 file changed, 77 insertions(+), 64 deletions(-) diff --git a/spec/client/ics-012-near-client/README.md b/spec/client/ics-012-near-client/README.md index 3ae06d510..4c60e7e56 100644 --- a/spec/client/ics-012-near-client/README.md +++ b/spec/client/ics-012-near-client/README.md @@ -36,7 +36,7 @@ type Signature = ED25519Signature `borsh` is a generic serialization function which follows the [Borsh serialization format](https://borsh.io/). -`merklize` is a generic function which can construct a merkle tree from an array which the element in it can be serialized by `borsh`. This function should return the merkle root of the tree at least. +`merklize` is a generic function which can construct a merkle tree from an array which the element in it can be serialized by `borsh`. This function should return the merkle root of the tree at least. (In this document, we assume this function can return a tuple. We use `merklize(...).root` to denote the merkle root of the result tree.) In NEAR protocol, the block producers are changed by time (about 12 hours). The period is known as `epoch` and the id of an epoch is represented by a `CryptoHash`. @@ -68,14 +68,14 @@ interface ClientState { The NEAR client tracks the block producers of current epoch and header (refer to [Headers section](#headers)) for all previously verified consensus states (these can be pruned after a certain period, but should not be pruned beforehand). ```typescript -interface ValidatorStakeView { - accountId: String +interface ValidatorStake { + accountId: string publicKey: PublicKey stake: uint128 } interface ConsensusState { - currentBps: List + currentBps: List header: Header } ``` @@ -85,9 +85,7 @@ interface ConsensusState { The height of a NEAR client is an `uint64` number. ```typescript -interface Height { - height: uint64 -} +type Height = uint64 ``` Comparison between heights is implemented as follows: @@ -104,7 +102,7 @@ function compare(a: Height, b: Height): Ord { ### Headers -The NEAR client headers include the `LightClientBlock` and previou state root of chunks. The entire block producers for next epoch and approvals for the block after the next are included in `LightClientBlock`. +The NEAR client headers include the `LightClientBlock` and previous state root of chunks. The entire block producers for next epoch and approvals for the block after the next are included in `LightClientBlock`. ```typescript interface BlockHeaderInnerLite { @@ -120,10 +118,10 @@ interface BlockHeaderInnerLite { interface LightClientBlock { prevBlockHash: CryptoHash - nextBlockInnerHash: CryptoHash, + nextBlockInnerHash: CryptoHash innerLite: BlockHeaderInnerLite innerRestHash: CryptoHash - nextBps: Maybe> + nextBps: Maybe> approvalsAfterNext: List> } @@ -133,7 +131,7 @@ interface Header { } ``` -The current block hash, next block hash and approval message can be calcuated from `LightClientBlock`. The signatures in `approvalsAfterNext` are provided by current block producers by signing the approval message. +The current block hash, next block hash and approval message can be calculated from `LightClientBlock`. The signatures in `approvalsAfterNext` are provided by current block producers by signing the approval message. ```typescript function (LightClientBlock) currentBlockHash(): CryptoHash { @@ -166,7 +164,7 @@ function (LightClientBlock) approvalMessage(): []byte { } ``` -We also defines the `CommitmentRoot` of `Header` as: +We also define the `CommitmentRoot` of `Header` as: ```typescript function (Header) commitmentRoot(): CryptoHash { @@ -206,14 +204,12 @@ function initialise( upgradeCommitmentPrefix: []byte, upgradeKey: []bype, consensusState: ConsensusState): ClientState { - assert(consensusState.currentBps.len > 0) + assert(len(consensusState.currentBps) > 0) assert(consensusState.header.getHeight() > 0) // implementations may define a identifier generation function identifier = generateClientIdentifier() - height = Height { - height: consensusState.header.getHeight() - } - set("clients/{identifier}/consensusStates/{consensusState.header.getHeight()}", consensusState) + height = consensusState.header.getHeight() + provableStore.set("clients/{identifier}/consensusStates/{height}", consensusState) return ClientState { trustingPeriod latestHeight: height @@ -249,7 +245,7 @@ function (Header) getNextEpochId(): CryptoHash { return self.lightClientBlock.innerLite.nextEpochId } -function (ConsensusState) getBlockProducersOf(epochId: CryptoHash): List { +function (ConsensusState) getBlockProducersOf(epochId: CryptoHash): List { if epochId === self.header.getEpochId() { return self.currentBps } else if epochId === self.header.getNextEpochId() { @@ -260,9 +256,9 @@ function (ConsensusState) getBlockProducersOf(epochId: CryptoHash): List bool { - clientState = get("clients/{clientMsg.identifier}/clientState") + clientState = provableStore.get("clients/{clientMsg.identifier}/clientState") switch typeof(clientMsg) { case Header: // fetch consensusstate at header height if it exists - consensusState = get("clients/{clientMsg.identifier}/consensusStates/{header.getHeight()}") + 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 @@ -357,26 +352,35 @@ function checkForMisbehaviour( return true } case Misbehaviour: - // assert that the heights are the same - assert(misbehaviour.header1.getHeight() === misbehaviour.header2.getHeight()) - // assert that the commitments are different - assert(misbehaviour.header1.commitmentRoot() !== misbehaviour.header2.commitmentRoot()) + if (misbehaviour.header1.getHeight() < misbehaviour.header2.getHeight()) { + return false + } + // if heights are equal check that this is valid misbehaviour of a fork + if (misbehaviour.header1.getHeight() === misbehaviour.header2.getHeight() && misbehaviour.header1.commitmentRoot() !== misbehaviour.header2.commitmentRoot()) { + return true + } + // otherwise if heights are unequal check that this is valid misbehavior of BFT time violation + if (misbehaviour.header1.timestamp() <= misbehaviour.header2.timestamp()) { + return true + } + + return false } } ``` > As the slashing policy is NOT applicable in NEAR protocol for now, this section is only for references. -### UpdateState +### Update State -UpdateState will perform a regular update for the NEAR client. It will add a consensus state to the client store. If the header is higher than the lastest height on the clientState, then the clientState will be updated. +Function `updateState` will perform a regular update for the NEAR client. It will add a consensus state to the client store. If the header is higher than the lastest height on the clientState, then the clientState will be updated. ```typescript function updateState(clientMessage: clientMessage) { - clientState = get("clients/{clientMessage.identifier}/clientState") - consensusState = get("clients/{clientMessage.identifier}/consensusStates/{clientState.latestHeight}") + clientState = provableStore.get("clients/{clientMessage.identifier}/clientState") + consensusState = provableStore.get("clients/{clientMessage.identifier}/consensusStates/{clientState.latestHeight}") - header = clientMessage.getHeader() + header = Header(clientMessage) // only update the clientstate if the header height is higher // than clientState latest height if clientState.latestHeight < header.getHeight() { @@ -384,34 +388,32 @@ function updateState(clientMessage: clientMessage) { clientState.latestHeight = header.getHeight() // save the client - set("clients/{clientMessage.identifier}/clientState", clientState) + provableStore.set("clients/{clientMessage.identifier}/clientState", clientState) } currentBps = consensusState.getBlockProducersOf(header.getEpochId()) // create recorded consensus state, save it newConsensusState = ConsensusState { currentBps, header } - set("clients/{clientMessage.identifier}/consensusStates/{header.getHeight()}", newConsensusState) + provableStore.set("clients/{clientMessage.identifier}/consensusStates/{header.getHeight()}", newConsensusState) } ``` -### UpdateStateOnMisbehaviour +### Update State On Misbehaviour -UpdateStateOnMisbehaviour will set the frozen height to a non-zero sentinel height to freeze the entire client. +Function `updateStateOnMisbehaviour` will set the frozen height to a non-zero sentinel height to freeze the entire client. ```typescript function updateStateOnMisbehaviour(clientMsg: clientMessage) { - clientState = get("clients/{clientMsg.identifier}/clientState") - if checkForMisbehaviour(clientMsg) === true { - switch typeof(clientMsg) { - case Header: - prevConsState = getPreviousConsensusState(header.getHeight()) - clientState.frozenHeight = prevConsState.header.getHeight() - case Misbehaviour: - prevConsState = getPreviousConsensusState(clientMessage.header1.getHeight()) - clientState.frozenHeight = prevConsState.header.getHeight() - } + clientState = provableStore.get("clients/{clientMsg.identifier}/clientState") + switch typeof(clientMsg) { + case Header: + prevConsState = getPreviousConsensusState(header.getHeight()) + clientState.frozenHeight = prevConsState.header.getHeight() + case Misbehaviour: + prevConsState = getPreviousConsensusState(misbehaviour.header1.getHeight()) + clientState.frozenHeight = prevConsState.header.getHeight() } - set("clients/{clientMsg.identifier}/clientState", clientState) + provableStore.set("clients/{clientMsg.identifier}/clientState", clientState) } ``` @@ -431,19 +433,22 @@ function upgradeClientState( proof: CommitmentProof) { // assert trusting period has not yet passed assert(currentTimestamp() - clientState.latestTimestamp < clientState.trustingPeriod) - // check that the revision has been incremented + // check that the latest height has been incremented assert(newClientState.latestHeight > clientState.latestHeight) // 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) // fetch the previously verified commitment root & verify membership - root = get("clients/{clientMessage.identifier}/consensusStates/{height}") - // verify that the provided consensus state has been stored - assert(root.verifyMembership(path, newClientState, proof)) + // 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("{clientIdentifier}/consensusStates/{height}") + // verify that the provided client state has been stored + assert(consensusState.verifyMembership(path, newClientState, proof)) // update client state clientState = newClientState - set("clients/{clientMessage.identifier}/clientState", clientState) + provableStore.set("{clientIdentifier}/clientState", clientState) } ``` @@ -459,7 +464,8 @@ function (ConsensusState) verifyMembership( // Check that the root in proof is one of the prevStateRoot of a chunk assert(hash(proof[0]) in self.header.prevStateRootOfChunks) // Check the value on the path is exactly the given value with proof data - // based on MPT construction algorithm + // based on MPT construction algorithm. + // Omit pseudocode for the verification in this document. } function verifyMembership( @@ -469,7 +475,7 @@ function verifyMembership( delayBlockPeriod: uint64, proof: CommitmentProof, path: CommitmentPath, - value: []byte) { + 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 @@ -482,9 +488,12 @@ function verifyMembership( // 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 - header = get("clients/{clientMessage.identifier}/consensusStates/{height}") + consensusState = provableStore.get("clients/{clientMessage.identifier}/consensusStates/{height}") // verify that has been stored - assert(header.verifyMembership(path, value, proof)) + if !consensusState.verifyMembership(path, value, proof) { + return Error + } + return nil } function (ConsensusState) verifyNonMembership( @@ -493,7 +502,8 @@ function (ConsensusState) verifyNonMembership( // Check that the root in proof is one of the prevStateRoot of a chunk assert(hash(proof[0]) in self.header.prevStateRootOfChunks) // Check that there is NO value on the path with proof data - // based on MPT construction algorithm + // based on MPT construction algorithm. + // Omit pseudocode for the verification in this document. } function verifyNonMembership( @@ -502,7 +512,7 @@ function verifyNonMembership( delayTimePeriod: uint64, delayBlockPeriod: uint64, proof: CommitmentProof, - path: CommitmentPath) { + 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 @@ -515,9 +525,12 @@ function verifyNonMembership( // 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 - header = get("clients/{clientMessage.identifier}/consensusStates/{height}") + consensusState = provableStore.get("clients/{clientMessage.identifier}/consensusStates/{height}") // verify that nothing has been stored at path - assert(header.verifyNonMembership(path, proof)) + if !consensusState.verifyNonMembership(path, proof) { + return Error + } + return nil } ```