Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ICS-06: Solomachine Refactor #821

Merged
merged 13 commits into from
Jan 9, 2023
310 changes: 152 additions & 158 deletions spec/client/ics-006-solo-machine-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,32 @@ interface Header {
}
```

AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
`Header` implements the ClientMessage interface.
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved

### 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
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved

`Misbehaviour` for solo machines consists of a sequence and two signatures over different messages at that sequence.

```typescript
interface SignatureAndData {
sig: Signature
path: Path
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
data: []byte
timestamp: Timestamop
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
}

interface Misbehaviour {
Expand All @@ -95,6 +113,8 @@ interface Misbehaviour {
}
```

`Misbehaviour` implements the ClientState interface.
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved

### Signatures

Signatures are provided in the `Proof` field of client state verification functions. They include data & a timestamp, which must also be signed over.
Expand Down Expand Up @@ -127,205 +147,178 @@ function latestClientHeight(clientState: ClientState): uint64 {
}
```

### ClientState Methods

All of the functions defined below are methods on the `ClientState` interface. Thus, the solomachine clientstate is always in scope for these functions.
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved

### 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 and diversifier signed over the client message at the expected sequence. 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.
Copy link
Member

@damiannolan damiannolan Dec 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit on the wording here, what about something like:

The solo machine client verifyClientMessage function checks that the private key associated with the current public key has signed over the client message using the current diversifier and expected sequence.

The diversifier is part of the client message sign bytes which we verify using the pub key, right?
I think it should also be clear that the private key does the actual signing

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the wording with public key is consistent with other documentation regarding public-private key cryptography. Reworded to clarify the diversifier part


```typescript
function checkValidityAndUpdateState(
clientState: ClientState,
header: Header) {
assert(header.sequence === clientState.consensusState.sequence)
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++
function verifyClientMessage(clientMsg: ClientMessage) {
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this PR (assuming it gets merged) we are changing the signature of 07-tendermint's verifyClientMessage to return a boolean. Should we do a similar update here then?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually prefer the error at least in implementations, since it bubbles up more information to user

switch typeof(ClientMessage) {
case Header:
verifyHeader(clientMessage)
// misbehaviour only suppported for current public key and diversifier on solomachine
case Misbehaviour:
verifyMisbehaviour(clientMessage)
}
}
```

### Misbehaviour predicate

Any duplicate signature on different messages by the current public key freezes a solo machine client.
function verifyHeader(header: header) {
assert(header.timestamp >= clientstate.consensusState.timestamp)
crodriguezvega marked this conversation as resolved.
Show resolved Hide resolved
headerData = {
NewPublicKey: header.newPublicKey,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe not relevant, but in ibc-go this field is called NewPubKey..., should we have a consistent name on both spec and implementation?

NewDiversifier: header.newDiversifier,
}
sigBytes = SignBytes(
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
Sequence: clientState.consensusState.sequence,
Timestamp: header.timestamp,
Diversifier: clientState.consensusState.diversifier,
Path: []byte{"SENTINEL_HEADER_PATH"}
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
Value: marshal(headerData)
)
assert(checkSignature(cs.consensusState.publicKey, sigBytes, header.signature))
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
}

```typescript
function checkMisbehaviourAndUpdateState(
clientState: ClientState,
misbehaviour: Misbehaviour) {
h1 = misbehaviour.h1
h2 = misbehaviour.h2
function verifyMisbehaviour(misbehaviour: Misbehaviour) {
s1 = misbehaviour.signatureOne
s2 = misbehaviour.signatureTwo
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
assert(misbehaviour.s1.timestamp >= timestamp)
assert(misbehaviour.s2.timestamp >= timestamp)
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
// 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
)
assert(sigBytes1 != sigBytes2)
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
assert(checkSignature(pubkey, sigBytes1, clientState.consensusState.publicKey))
assert(checkSignature(pubkey, sigBytes2, clientState.consensusState.publicKey))
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
}
```

### State verification functions

All solo machine client state verification functions simply check a signature, which must be provided by the solo machine.
### Misbehaviour predicate

Note that value concatenation should be implemented in a state-machine-specific escaped fashion.
Since misbehaviour is checked in `verifyClientMessage`, if the client message is of type `Misbehaviour` then we return true
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved

```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 checkForMisbehaviour(clientMessage: ClientMessage) => bool {
switch typeof(ClientMessage) {
case Misbehaviour:
return true
}
return false
}
```

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
}
### Update Functions

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
}
`UpdateState` updates the function for a regular update:
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved

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))
```typescript
function updateState(clientMessage: ClientMessage) {
clientState.consensusState.publicKey = header.newPublicKey
clientState.consensusState.diversifier = header.newDiversifier
clientState.consensusState.timestamp = header.timestamp
clientState.consensusState.sequence++
clientState.consensusState.timestamp = proof.timestamp
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
}
`UpdateStateOnMisbehaviour` updates the function after receving valid misbehaviour:

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
```typescript
function updateStateOnMisbehaviour(clientMessage: ClientMessage) {
// freeze the client
clientState.frozen = true
set("clients/{identifier}/clientState", clientState)
}
```

### State verification functions

All solo machine client state verification functions simply check a signature, which must be provided by the solo machine.

Note that value concatenation should be implemented in a state-machine-specific escaped fashion.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is meant by this exactly?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry this was from before. Now that we standardized how signbytes should look it is no longer relevant


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)
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
abortTransactionUnless(proof.timestamp >= clientState.consensusState.timestamp)
value = clientState.consensusState.sequence + clientState.consensusState.diversifier + proof.timestamp + path
assert(checkSignature(clientState.consensusState.pubKey, value, proof.sig))
sigBytes = SignBytes(
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
Sequence: clientState.consensusState.sequence,
Timestamp: proof.timestamp,
Diversifier: clientState.consensusState.diversifier,
path: path.String(),
data: value,
)
proven = checkSignature(clientState.consensusState.pubKey, sigBytes, proof.sig)
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
if !proven {
return false
}

// increment sequence on each verification to provide
// replay protection
clientState.consensusState.sequence++
clientState.consensusState.timestamp = proof.timestamp
return true
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
}

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))
sigBytes = SignBytes(
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
Sequence: clientState.consensusState.sequence,
Timestamp: proof.timestamp,
Diversifier: clientState.consensusState.diversifier,
path: path.String(),
data: nil,
)
proven = checkSignature(clientState.consensusState.pubKey, value, proof.sig)
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
if !proven {
return false
}

// increment sequence on each verification to provide
// replay protection
clientState.consensusState.sequence++
clientState.consensusState.timestamp = proof.timestamp
return true
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
}
```

Expand Down Expand Up @@ -353,6 +346,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

Expand Down