ics | title | stage | category | requires | required-by | author | created | modified |
---|---|---|---|---|---|---|---|---|
3 |
Connection Semantics |
draft |
IBC/TAO |
2, 24 |
4, 25 |
Christopher Goes <[email protected]>, Juwoon Yun <[email protected]> |
2019-03-07 |
2019-08-25 |
This standards document describes the abstraction of an IBC connection: two stateful objects (connection ends) on two separate chains, each associated with a light client of the other chain, which together facilitate cross-chain sub-state verification and packet association (through channels). A protocol for safely establishing a connection between two chains is described.
The core IBC protocol provides authorisation and ordering semantics for packets: guarantees, respectively, that packets have been committed on the sending blockchain (and according state transitions executed, such as escrowing tokens), and that they have been committed exactly once in a particular order and can be delivered exactly once in that same order. The connection abstraction specified in this standard, in conjunction with the client abstraction specified in ICS 2, defines the authorisation semantics of IBC. Ordering semantics are described in ICS 4).
Client-related types & functions are as defined in ICS 2.
Commitment proof related types & functions are defined in ICS 23
Identifier
and other host state machine requirements are as defined in ICS 24. The identifier is not necessarily intended to be a human-readable name (and likely should not be, to discourage squatting or racing for identifiers).
The opening handshake protocol allows each chain to verify the identifier used to reference the connection on the other chain, enabling modules on each chain to reason about the reference on the other chain.
An actor, as referred to in this specification, is an entity capable of executing datagrams who is paying for computation / storage (via gas or a similar mechanism) but is otherwise untrusted. Possible actors include:
- End users signing with an account key
- On-chain smart contracts acting autonomously or in response to another transaction
- On-chain modules acting in response to another transaction or in a scheduled manner
- Implementing blockchains should be able to safely allow untrusted actors to open and update connections.
Prior to connection establishment:
- No further IBC sub-protocols should operate, since cross-chain sub-states cannot be verified.
- The initiating actor (who creates the connection) must be able to specify an initial consensus state for the chain to connect to and an initial consensus state for the connecting chain (implicitly, e.g. by sending the transaction).
Once a negotiation handshake has begun:
- Only the appropriate handshake datagrams can be executed in order.
- No third chain can masquerade as one of the two handshaking chains
Once a negotiation handshake has completed:
- The created connection objects on both chains contain the consensus states specified by the initiating actor.
- No other connection objects can be maliciously created on other chains by replaying datagrams.
This ICS defines the ConnectionState
and ConnectionEnd
types:
enum ConnectionState {
INIT,
TRYOPEN,
OPEN,
}
interface ConnectionEnd {
state: ConnectionState
counterpartyConnectionIdentifier: Identifier
counterpartyPrefix: CommitmentPrefix
clientIdentifier: Identifier
counterpartyClientIdentifier: Identifier
version: string | []string
}
- The
state
field describes the current state of the connection end. - The
counterpartyConnectionIdentifier
field identifies the connection end on the counterparty chain associated with this connection. - The
clientIdentifier
field identifies the client associated with this connection. - The
counterpartyClientIdentifier
field identifies the client on the counterparty chain associated with this connection. - The
version
field is an opaque string which can be utilised to determine encodings or protocols for channels or packets utilising this connection.
Connection paths are stored under a unique identifier.
function connectionPath(id: Identifier): Path {
return "connections/{id}"
}
A reverse mapping from clients to a set of connections (utilised to look up all connections using a client) is stored under a unique prefix per-client:
function clientConnectionsPath(clientIdentifier: Identifier): Path {
return "clients/{clientIdentifier}/connections"
}
addConnectionToClient
is used to add a connection identifier to the set of connections associated with a client.
function addConnectionToClient(
clientIdentifier: Identifier,
connectionIdentifier: Identifier) {
conns = privateStore.get(clientConnectionsPath(clientIdentifier))
conns.add(connectionIdentifier)
privateStore.set(clientConnectionsPath(clientIdentifier), conns)
}
removeConnectionFromClient
is used to remove a connection identifier from the set of connections associated with a client.
function removeConnectionFromClient(
clientIdentifier: Identifier,
connectionIdentifier: Identifier) {
conns = privateStore.get(clientConnectionsPath(clientIdentifier, connectionIdentifier))
conns.remove(connectionIdentifier)
privateStore.set(clientConnectionsPath(clientIdentifier, connectionIdentifier), conns)
}
Two helper functions are defined to provide automatic application of CommitmentPrefix
. In the other parts of the specifications,
these functions MUST be used for introspecting other chains' state, instead of directly calling the verifyMembership
or verifyNonMembership
function on the client.
function verifyMembership(
connection: ConnectionEnd,
height: uint64,
proof: CommitmentProof,
path: Path,
value: Value): bool {
client = queryClient(connection.clientIdentifier)
client.verifyMembership(height, proof, applyPrefix(connection.counterpartyPrefix, path), value)
}
function verifyNonMembership(
connection: ConnectionEnd,
height: uint64,
proof: CommitmentProof,
path: Path): bool {
client = queryClient(connection.clientIdentifier)
client.verifyNonMembership(height, proof, applyPrefix(connection.counterpartyPrefix, path))
}
This ICS defines the opening handshake subprotocol. Once opened, connections cannot be closed and identifiers cannot be reallocated (this prevents packet replay or authorisation confusion).
Header tracking and misbehaviour detection are defined in ICS 2.
Connections are stored under a unique Identifier
prefix.
The validation function validateConnectionIdentifier
MAY be provided.
type validateConnectionIdentifier = (id: Identifier) => boolean
If not provided, the default validateConnectionIdentifier
function will always return true
.
During the handshake process, two ends of a connection come to agreement on a version bytestring associated with that connection. At the moment, the contents of this version bytestring are opaque to the IBC core protocol. In the future, it might be used to indicate what kinds of channels can utilise the connection in question, or what encoding formats channel-related datagrams will use. At present, host state machine MAY utilise the version data to negotiate encodings, priorities, or connection-specific metadata related to custom logic on top of IBC.
Host state machines MAY also safely ignore the version data or specify an empty string.
An implementation MUST define a function getCompatibleVersions
which returns the list of versions it supports, ranked by descending preference order.
type getCompatibleVersions = () => []string
An implementation MUST define a function pickVersion
to choose a version from a list of versions proposed by a counterparty.
type pickVersion = ([]string) => string
The opening handshake sub-protocol serves to initialise consensus states for two chains on each other.
The opening handshake defines four datagrams: ConnOpenInit, ConnOpenTry, ConnOpenAck, and ConnOpenConfirm.
A correct protocol execution flows as follows (note that all calls are made through modules per ICS 25):
Initiator | Datagram | Chain acted upon | Prior state (A, B) | Posterior state (A, B) |
---|---|---|---|---|
Actor | ConnOpenInit |
A | (none, none) | (INIT, none) |
Relayer | ConnOpenTry |
B | (INIT, none) | (INIT, TRYOPEN) |
Relayer | ConnOpenAck |
A | (INIT, TRYOPEN) | (OPEN, TRYOPEN) |
Relayer | ConnOpenConfirm |
B | (OPEN, TRYOPEN) | (OPEN, OPEN) |
At the end of an opening handshake between two chains implementing the sub-protocol, the following properties hold:
- Each chain has each other's correct consensus state as originally specified by the initiating actor.
- Each chain has knowledge of and has agreed to its identifier on the other chain.
This sub-protocol need not be permissioned, modulo anti-spam measures.
ConnOpenInit initialises a connection attempt on chain A.
function connOpenInit(
identifier: Identifier,
desiredCounterpartyConnectionIdentifier: Identifier,
counterpartyPrefix: CommitmentPrefix,
clientIdentifier: Identifier,
counterpartyClientIdentifier: Identifier) {
abortTransactionUnless(validateConnectionIdentifier(identifier))
abortTransactionUnless(provableStore.get(connectionPath(identifier)) == null)
state = INIT
connection = ConnectionEnd{state, desiredCounterpartyConnectionIdentifier, counterpartyPrefix,
clientIdentifier, counterpartyClientIdentifier, getCompatibleVersions()}
provableStore.set(connectionPath(identifier), connection)
addConnectionToClient(clientIdentifier, identifier)
}
ConnOpenTry relays notice of a connection attempt on chain A to chain B (this code is executed on chain B).
function connOpenTry(
desiredIdentifier: Identifier,
counterpartyConnectionIdentifier: Identifier,
counterpartyPrefix: CommitmentPrefix,
counterpartyClientIdentifier: Identifier,
clientIdentifier: Identifier,
counterpartyVersions: string[],
proofInit: CommitmentProof,
proofHeight: uint64,
consensusHeight: uint64) {
abortTransactionUnless(validateConnectionIdentifier(desiredIdentifier))
abortTransactionUnless(consensusHeight <= getCurrentHeight())
expectedConsensusState = getConsensusState(consensusHeight)
expected = ConnectionEnd{INIT, desiredIdentifier, getCommitmentPrefix(), counterpartyClientIdentifier,
clientIdentifier, counterpartyVersions}
version = pickVersion(counterpartyVersions)
connection = ConnectionEnd{state, counterpartyConnectionIdentifier, counterpartyPrefix,
clientIdentifier, counterpartyClientIdentifier, version}
abortTransactionUnless(
connection.verifyMembership(proofHeight, proofInit,
connectionPath(counterpartyConnectionIdentifier),
expected))
abortTransactionUnless(
connection.verifyMembership(proofHeight, proofInit,
consensusStatePath(counterpartyClientIdentifier),
expectedConsensusState))
abortTransactionUnless(provableStore.get(connectionPath(desiredIdentifier)) === null)
identifier = desiredIdentifier
state = TRYOPEN
provableStore.set(connectionPath(identifier), connection)
addConnectionToClient(clientIdentifier, identifier)
}
ConnOpenAck relays acceptance of a connection open attempt from chain B back to chain A (this code is executed on chain A).
function connOpenAck(
identifier: Identifier,
version: string,
proofTry: CommitmentProof,
proofHeight: uint64,
consensusHeight: uint64) {
abortTransactionUnless(consensusHeight <= getCurrentHeight())
connection = provableStore.get(connectionPath(identifier))
abortTransactionUnless(connection.state === INIT)
expectedConsensusState = getConsensusState(consensusHeight)
expected = ConnectionEnd{TRYOPEN, identifier, getCommitmentPrefix(),
connection.counterpartyClientIdentifier, connection.clientIdentifier,
version}
abortTransactionUnless(
connection.verifyMembership(proofHeight, proofTry,
connectionPath(connection.counterpartyConnectionIdentifier),
expected))
abortTransactionUnless(
connection.verifyMembership(proofHeight, proofTry,
consensusStatePath(connection.counterpartyClientIdentifier),
expectedConsensusState))
connection.state = OPEN
abortTransactionUnless(getCompatibleVersions().indexOf(version) !== -1)
connection.version = version
provableStore.set(connectionPath(identifier), connection)
}
ConnOpenConfirm confirms opening of a connection on chain A to chain B, after which the connection is open on both chains (this code is executed on chain B).
function connOpenConfirm(
identifier: Identifier,
proofAck: CommitmentProof,
proofHeight: uint64) {
connection = provableStore.get(connectionPath(identifier))
abortTransactionUnless(connection.state === TRYOPEN)
expected = ConnectionEnd{OPEN, identifier, getCommitmentPrefix(), connection.counterpartyClientIdentifier,
connection.clientIdentifier, connection.version}
abortTransactionUnless(
connection.verifyMembership(proofHeight, proofAck,
connectionPath(connection.counterpartyConnectionIdentifier),
expected))
connection.state = OPEN
provableStore.set(connectionPath(identifier), connection)
}
Connections can be queried by identifier with queryConnection
.
function queryConnection(id: Identifier): ConnectionEnd | void {
return provableStore.get(connectionPath(id))
}
Connections associated with a particular client can be queried by client identifier with queryClientConnections
.
function queryClientConnections(id: Identifier): Set<Identifier> {
return privateStore.get(clientConnectionsPath(id))
}
- Connection identifiers are first-come-first-serve: once a connection has been negotiated, a unique identifier pair exists between two chains.
- The connection handshake cannot be man-in-the-middled by another blockchain's IBC handler.
Not applicable.
A future version of this ICS will include version negotiation in the opening handshake. Once a connection has been established and a version negotiated, future version updates can be negotiated per ICS 6.
The consensus state can only be updated as allowed by the updateConsensusState
function defined by the consensus protocol chosen when the connection is established.
Coming soon.
Coming soon.
Parts of this document were inspired by the previous IBC specification.
29 March 2019 - Initial draft version submitted 17 May 2019 - Draft finalised 29 July 2019 - Revisions to track connection set associated with client
All content herein is licensed under Apache 2.0.