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
291 changes: 140 additions & 151 deletions spec/client/ics-006-solo-machine-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,22 +70,37 @@ The `Height` of a solo machine is just a `uint64`, with the usual comparison ope

```typescript
interface Header {
sequence: uint64
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
timestamp: uint64
signature: Signature
newPublicKey: PublicKey
newDiversifier: string
}
```

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 Down Expand Up @@ -129,201 +144,174 @@ function latestClientHeight(clientState: ClientState): uint64 {

### 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(
function verifyClientMessage(
clientState: ClientState,
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
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++
clientMsg: ClientMessage) {
switch typeof(ClientMessage) {
case Header:
verifyHeader(clientState, clientMessage)
// misbehaviour only suppported for current public key and diversifier on solomachine
case Misbehaviour:
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
}
}
```

### Misbehaviour predicate

Any duplicate signature on different messages by the current public key freezes a solo machine client.
function verifyHeader(
clientState: clientState,
clientMessage: clientMessage) {
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,
function verifyMisbehaviour(
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
clientState: clientState,
misbehaviour: Misbehaviour) {
h1 = misbehaviour.h1
h2 = misbehaviour.h2
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: cs.consensusState.diversifier,
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
Path: s1.path,
Data: s1.data
)
sigBytes2 = SignBytes(
Sequence: misbehaviour.sequence,
Timestamp: s2.timestamp,
Diversifier: cs.consensusState.diversifier,
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
Path: s2.path,
Data: s2,.data
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
)
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
### Misbehaviour predicate

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.
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(
function checkForMisbehaviour(
clientState: ClientState,
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
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
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(
```typescript
function updateState(
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
clientState: ClientState,
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
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))
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
}
```

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(
```typescript
function updateStateOnMisbehaviour(
clientState: ClientState,
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
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
clientMessage: ClientMessage) {
// freeze the client
clientState.frozen = true
}
```

### State verification functions

function verifyPacketReceiptAbsence(
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


```typescript
function verifyMembership(
clientState: ClientState,
// 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) {
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
// 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: CommitmentPath.String(),
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
data: value,
)
assert(checkSignature(clientState.consensusState.pubKey, sigBytes, proof.sig))

// increment sequence on each verification to provide
// replay protection
clientState.consensusState.sequence++
clientState.consensusState.timestamp = proof.timestamp
}

function verifyNextSequenceRecv(
function verifyNonMembership(
clientState: ClientState,
// 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) {
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
abortTransactionUnless(!clientState.frozen)
abortTransactionUnless(proof.timestamp >= clientState.consensusState.timestamp)
value = clientState.consensusState.sequence + clientState.consensusState.diversifier + proof.timestamp + path + nextSequenceRecv
sigBytes = SignBytes(
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
Sequence: clientState.consensusState.sequence,
Timestamp: proof.timestamp,
Diversifier: clientState.consensusState.diversifier,
path: CommitmentPath.String(),
AdityaSripal marked this conversation as resolved.
Show resolved Hide resolved
data: nil,
)
assert(checkSignature(clientState.consensusState.pubKey, value, proof.sig))

// increment sequence on each verification to provide
// replay protection
clientState.consensusState.sequence++
clientState.consensusState.timestamp = proof.timestamp
}
Expand Down Expand Up @@ -353,6 +341,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