diff --git a/spec/client/ics-006-solo-machine-client/README.md b/spec/client/ics-006-solo-machine-client/README.md index 00932917b..144084cbd 100644 --- a/spec/client/ics-006-solo-machine-client/README.md +++ b/spec/client/ics-006-solo-machine-client/README.md @@ -70,7 +70,7 @@ The `Height` of a solo machine is just a `uint64`, with the usual comparison ope ```typescript interface Header { - sequence: uint64 + sequence: uint64 // deprecated timestamp: uint64 signature: Signature newPublicKey: PublicKey @@ -78,6 +78,22 @@ interface Header { } ``` +`Header` implements the `ClientMessage` interface. + +### Signature Verification + +The solomachine public key must sign over the following struct: + +```typescript +interface SignBytes { + sequence: uint64 + timestamp: uint64 + diversifier: string + path: []byte + data: []byte +} +``` + ### Misbehaviour `Misbehaviour` for solo machines consists of a sequence and two signatures over different messages at that sequence. @@ -85,7 +101,9 @@ interface Header { ```typescript interface SignatureAndData { sig: Signature + path: []byte data: []byte + timestamp: Timestamp } interface Misbehaviour { @@ -95,6 +113,8 @@ interface Misbehaviour { } ``` +`Misbehaviour` implements the `ClientMessage` interface. + ### Signatures Signatures are provided in the `Proof` field of client state verification functions. They include data & a timestamp, which must also be signed over. @@ -127,205 +147,184 @@ function latestClientHeight(clientState: ClientState): uint64 { } ``` +### ClientState Methods + +All of the functions defined below are methods on the `ClientState` interface. Thus, the solo machine client state is always in scope for these functions. + ### Validity predicate -The solo machine client `checkValidityAndUpdateState` function checks that the currently registered public key has signed over the new public key with the correct sequence. +The solo machine client `verifyClientMessage` function checks that the currently registered public key signed over the client message at the expected sequence with the current diversifier included in the client message. If the client message is an update, then it must be the current sequence. If the client message is misbehaviour then it must be the sequence of the misbehaviour. ```typescript -function checkValidityAndUpdateState( - clientState: ClientState, - header: Header) { - assert(header.sequence === clientState.consensusState.sequence) +function verifyClientMessage(clientMsg: ClientMessage) { + switch typeof(ClientMessage) { + case Header: + verifyHeader(clientMessage) + // misbehaviour only suppported for current public key and diversifier on solomachine + case Misbehaviour: + verifyMisbehaviour(clientMessage) + } +} + +function verifyHeader(header: header) { assert(header.timestamp >= clientstate.consensusState.timestamp) - assert(checkSignature(header.newPublicKey, header.sequence, header.diversifier, header.signature)) - clientState.consensusState.publicKey = header.newPublicKey - clientState.consensusState.diversifier = header.newDiversifier - clientState.consensusState.timestamp = header.timestamp - clientState.consensusState.sequence++ + headerData = { + newPubKey: header.newPubKey, + newDiversifier: header.newDiversifier, + } + signBytes = SignBytes( + sequence: clientState.consensusState.sequence, + timestamp: header.timestamp, + diversifier: clientState.consensusState.diversifier, + path: []byte{"solomachine:header"}, + value: marshal(headerData) + ) + assert(checkSignature(cs.consensusState.publicKey, signBytes, header.signature)) +} + +function verifyMisbehaviour(misbehaviour: Misbehaviour) { + s1 = misbehaviour.signatureOne + s2 = misbehaviour.signatureTwo + pubkey = clientState.consensusState.publicKey + diversifier = clientState.consensusState.diversifier + timestamp = clientState.consensusState.timestamp + // assert that the signatures validate and that they are different + sigBytes1 = SignBytes( + sequence: misbehaviour.sequence, + timestamp: s1.timestamp, + diversifier: diversifier, + path: s1.path, + data: s1.data + ) + sigBytes2 = SignBytes( + sequence: misbehaviour.sequence, + timestamp: s2.timestamp, + diversifier: diversifier, + path: s2.path, + data: s2.data + ) + // either the path or data must be different in order for the misbehaviour to be valid + assert(s1.path != s2.path || s1.data != s2.data) + assert(checkSignature(pubkey, sigBytes1, misbehaviour.signatureOne.signature)) + assert(checkSignature(pubkey, sigBytes2, misbehaviour.signatureTwo.signature)) } ``` ### Misbehaviour predicate -Any duplicate signature on different messages by the current public key freezes a solo machine client. +Since misbehaviour is checked in `verifyClientMessage`, if the client message is of type `Misbehaviour` then we return true: ```typescript -function checkMisbehaviourAndUpdateState( - clientState: ClientState, - misbehaviour: Misbehaviour) { - h1 = misbehaviour.h1 - h2 = misbehaviour.h2 - pubkey = clientState.consensusState.publicKey - diversifier = clientState.consensusState.diversifier - timestamp = clientState.consensusState.timestamp - // assert that timestamp could have fooled the light client - assert(misbehaviour.h1.signature.timestamp >= timestamp) - assert(misbehaviour.h2.signature.timestamp >= timestamp) - // assert that signature data is different - assert(misbehaviour.h1.signature.data !== misbehaviour.h2.signature.data) - // assert that the signatures validate - assert(checkSignature(pubkey, misbehaviour.sequence, diversifier, misbehaviour.h1.signature.data)) - assert(checkSignature(pubkey, misbehaviour.sequence, diversifier, misbehaviour.h2.signature.data)) - // freeze the client - clientState.frozen = true +function checkForMisbehaviour(clientMessage: ClientMessage) => bool { + switch typeof(ClientMessage) { + case Misbehaviour: + return true + } + return false } ``` -### State verification functions - -All solo machine client state verification functions simply check a signature, which must be provided by the solo machine. +### Update Functions -Note that value concatenation should be implemented in a state-machine-specific escaped fashion. +`UpdateState` updates the solo machine `ConsensusState` values using the provided client message header: ```typescript -function verifyClientState( - clientState: ClientState, - height: uint64, - prefix: CommitmentPrefix, - proof: CommitmentProof, - clientIdentifier: Identifier, - counterpartyClientState: ClientState) { - path = applyPrefix(prefix, "clients/{clientIdentifier}/clientState") - // ICS 003 will not increment the proof height after connection verification - // the solo machine client must increment the proof height to ensure it matches - // the expected sequence used in the signature - abortTransactionUnless(height + 1 == clientState.consensusState.sequence) - abortTransactionUnless(!clientState.frozen) - abortTransactionUnless(proof.timestamp >= clientState.consensusState.timestamp) - value = clientState.consensusState.sequence + clientState.consensusState.diversifier + proof.timestamp + path + counterpartyClientState - assert(checkSignature(clientState.consensusState.pubKey, value, proof.sig)) - clientState.consensusState.sequence++ - clientState.consensusState.timestamp = proof.timestamp -} - -function verifyClientConsensusState( - clientState: ClientState, - height: uint64, - prefix: CommitmentPrefix, - proof: CommitmentProof, - clientIdentifier: Identifier, - consensusStateHeight: uint64, - consensusState: ConsensusState) { - path = applyPrefix(prefix, "clients/{clientIdentifier}/consensusState/{consensusStateHeight}") - // ICS 003 will not increment the proof height after connection or client state verification - // the solo machine client must increment the proof height by 2 to ensure it matches - // the expected sequence used in the signature - abortTransactionUnless(height + 2 == clientState.consensusState.sequence) - abortTransactionUnless(!clientState.frozen) - abortTransactionUnless(proof.timestamp >= clientState.consensusState.timestamp) - value = clientState.consensusState.sequence + clientState.consensusState.diversifier + proof.timestamp + path + consensusState - assert(checkSignature(clientState.consensusState.pubKey, value, proof.sig)) - clientState.consensusState.sequence++ - clientState.consensusState.timestamp = proof.timestamp +function updateState(clientMessage: ClientMessage) { + clientState.consensusState.publicKey = header.newPubKey + clientState.consensusState.diversifier = header.newDiversifier + clientState.consensusState.timestamp = header.timestamp + clientState.consensusState.sequence++ + set("clients/{identifier}/clientState", clientState) } +``` -function verifyConnectionState( - clientState: ClientState, - height: uint64, - prefix: CommitmentPrefix, - proof: CommitmentProof, - connectionIdentifier: Identifier, - connectionEnd: ConnectionEnd) { - path = applyPrefix(prefix, "connection/{connectionIdentifier}") - abortTransactionUnless(height == clientState.consensusState.sequence) - abortTransactionUnless(!clientState.frozen) - abortTransactionUnless(proof.timestamp >= clientState.consensusState.timestamp) - value = clientState.consensusState.sequence + clientState.consensusState.diversifier + proof.timestamp + path + connectionEnd - assert(checkSignature(clientState.consensusState.pubKey, value, proof.sig)) - clientState.consensusState.sequence++ - clientState.consensusState.timestamp = proof.timestamp -} +`UpdateStateOnMisbehaviour` updates the function after receving valid misbehaviour: -function verifyChannelState( - clientState: ClientState, - height: uint64, - prefix: CommitmentPrefix, - proof: CommitmentProof, - portIdentifier: Identifier, - channelIdentifier: Identifier, - channelEnd: ChannelEnd) { - path = applyPrefix(prefix, "ports/{portIdentifier}/channels/{channelIdentifier}") - abortTransactionUnless(height == clientState.consensusState.sequence) - abortTransactionUnless(!clientState.frozen) - abortTransactionUnless(proof.timestamp >= clientState.consensusState.timestamp) - value = clientState.consensusState.sequence + clientState.consensusState.diversifier + proof.timestamp + path + channelEnd - assert(checkSignature(clientState.consensusState.pubKey, value, proof.sig)) - clientState.consensusState.sequence++ - clientState.consensusState.timestamp = proof.timestamp +```typescript +function updateStateOnMisbehaviour(clientMessage: ClientMessage) { + // freeze the client + clientState.frozen = true + set("clients/{identifier}/clientState", clientState) } +``` -function verifyPacketData( - clientState: ClientState, - height: uint64, - prefix: CommitmentPrefix, - proof: CommitmentProof, - portIdentifier: Identifier, - channelIdentifier: Identifier, - sequence: uint64, - data: bytes) { - path = applyPrefix(prefix, "ports/{portIdentifier}/channels/{channelIdentifier}/packets/{sequence}") - abortTransactionUnless(height == clientState.consensusState.sequence) - abortTransactionUnless(!clientState.frozen) - abortTransactionUnless(proof.timestamp >= clientState.consensusState.timestamp) - value = clientState.consensusState.sequence + clientState.consensusState.diversifier + proof.timestamp + path + data - assert(checkSignature(clientState.consensusState.pubKey, value, proof.sig)) - clientState.consensusState.sequence++ - clientState.consensusState.timestamp = proof.timestamp -} +### State verification functions -function verifyPacketAcknowledgement( - clientState: ClientState, - height: uint64, - prefix: CommitmentPrefix, - proof: CommitmentProof, - portIdentifier: Identifier, - channelIdentifier: Identifier, - sequence: uint64, - acknowledgement: bytes) { - path = applyPrefix(prefix, "ports/{portIdentifier}/channels/{channelIdentifier}/acknowledgements/{sequence}") - abortTransactionUnless(height == clientState.consensusState.sequence) - abortTransactionUnless(!clientState.frozen) - abortTransactionUnless(proof.timestamp >= clientState.consensusState.timestamp) - value = clientState.consensusState.sequence + clientState.consensusState.diversifier + proof.timestamp + path + acknowledgement - assert(checkSignature(clientState.consensusState.pubKey, value, proof.sig)) - clientState.consensusState.sequence++ - clientState.consensusState.timestamp = proof.timestamp -} +All solo machine client state verification functions simply check a signature, which must be provided by the solo machine. -function verifyPacketReceiptAbsence( - clientState: ClientState, +```typescript +function verifyMembership( + // provided height is unnecessary for solomachine + // since clientState maintains the expected sequence height: uint64, - prefix: CommitmentPrefix, + // delayPeriod is unsupported on solomachines + // thus these fields are ignored + delayTimePeriod: uint64, + delayBlockPeriod: uint64, proof: CommitmentProof, - portIdentifier: Identifier, - channelIdentifier: Identifier, - sequence: uint64) { - path = applyPrefix(prefix, "ports/{portIdentifier}/channels/{channelIdentifier}/receipts/{sequence}") - abortTransactionUnless(height == clientState.consensusState.sequence) + path: CommitmentPath, + value: []byte): boolean { + // the expected sequence used in the signature abortTransactionUnless(!clientState.frozen) abortTransactionUnless(proof.timestamp >= clientState.consensusState.timestamp) - value = clientState.consensusState.sequence + clientState.consensusState.diversifier + proof.timestamp + path - assert(checkSignature(clientState.consensusState.pubKey, value, proof.sig)) + signBytes = SignBytes( + sequence: clientState.consensusState.sequence, + timestamp: proof.timestamp, + diversifier: clientState.consensusState.diversifier, + path: path.String(), + data: value, + ) + proven = checkSignature(clientState.consensusState.publicKey, signBytes, proof.sig) + if !proven { + return false + } + + // increment sequence on each verification to provide + // replay protection clientState.consensusState.sequence++ clientState.consensusState.timestamp = proof.timestamp + // unlike other clients, we must set the client state here because we + // mutate the clientState (increment sequence and set timestamp) + // thus the verification methods are stateful for the solomachine + // in order to prevent replay attacks + set("clients/{identifier}/clientState", clientState) + return true } -function verifyNextSequenceRecv( - clientState: ClientState, +function verifyNonMembership( + // provided height is unnecessary for solomachine + // since clientState maintains the expected sequence height: uint64, - prefix: CommitmentPrefix, + // delayPeriod is unsupported on solomachines + // thus these fields are ignored + delayTimePeriod: uint64, + delayBlockPeriod: uint64, proof: CommitmentProof, - portIdentifier: Identifier, - channelIdentifier: Identifier, - nextSequenceRecv: uint64) { - path = applyPrefix(prefix, "ports/{portIdentifier}/channels/{channelIdentifier}/nextSequenceRecv") - abortTransactionUnless(height == clientState.consensusState.sequence) + path: CommitmentPath): boolean { abortTransactionUnless(!clientState.frozen) abortTransactionUnless(proof.timestamp >= clientState.consensusState.timestamp) - value = clientState.consensusState.sequence + clientState.consensusState.diversifier + proof.timestamp + path + nextSequenceRecv - assert(checkSignature(clientState.consensusState.pubKey, value, proof.sig)) + signBytes = SignBytes( + sequence: clientState.consensusState.sequence, + timestamp: proof.timestamp, + diversifier: clientState.consensusState.diversifier, + path: path.String(), + data: nil, + ) + proven = checkSignature(clientState.consensusState.publicKey, signBytes, proof.sig) + if !proven { + return false + } + + // increment sequence on each verification to provide + // replay protection clientState.consensusState.sequence++ clientState.consensusState.timestamp = proof.timestamp + // unlike other clients, we must set the client state here because we + // mutate the clientState (increment sequence and set timestamp) + // thus the verification methods are stateful for the solomachine + // in order to prevent replay attacks + set("clients/{identifier}/clientState", clientState) + return true } ``` @@ -353,6 +352,7 @@ None at present. December 9th, 2019 - Initial version December 17th, 2019 - Final first draft +August 15th, 2022 - Changes to align with 02-client-refactor in [\#813](https://github.com/cosmos/ibc/pull/813) ## Copyright