Suppose there are two chains (e.g., distributed ledgers) that execute independently of each other. What should be an abstraction for representing a path of communication between the two? We use a connection abstraction to represent this path of communication (see the ICS 003 spec).
The IBC connection handshake protocol, at the highest level of abstraction, enables two chains to establish -- i.e., open -- a connection with each other. Once open, connections cannot be closed, so a closing handshake does not exist. Whenever we speak of connection handshake, we implicitly mean the opening handshake. Throughout the rest of this document, we will often use the abbreviation "ICS3" to stand for connection handshake problem (or protocol, depending on the context).
A connection involves the two parties (the respective chains) involved in the communication, as well as a relayer which handles message transmission between the chains. The relayer carries a central responsibility in handshaking and maintaining a connection. That is, a connection handshake is essentially a three-party protocol: two chains, plus a relayer. This document focuses on the functionality of the two chains, while providing a minimal description of the relayer.
A chain comprises three elements that are relevant for connections:
- a module, or the "connection handshake module" or "ICS3 module": this is a process (running as a component of the chain) which implements the ICS3 protocol;
- the store of the chain, alternatively called the "consensus state" or just "state" of the chain;
- a client: a process running on the chain, which represents a window onto the store of the other chain.
We give concrete names, Alice and Bob, to the chains involved in our problem, to simplify description. The diagram below sketches the system model of the connection handshake problem. Note that the client on Alice points to the store on Bob; vice versa is true for the client on Bob (but there is no corresponding arrow to keep the figure cleaner). Alice and Bob have no direct medium for communicating with each other, so a relayer process sits between the two chains and enables their communication (more details on this will follow).
IBC Connection Handshake
High-level Model
Alice Bob
+-----------+ +-----------+
| Chain | +---------+ | Chain |
| |<--->| Relayer |<--->| |
| +-------+ | +---------+ | +-------+ |
| |Module | | | |Module | |
| +-------+ | | +-------+ |
| +-------+ | | +-------+ |
| | Store | | -->| Store | |
| +-------+ | -------/ | +-------+ |
|+---------+| --------/ |+---------+|
|| Client |---/ || Client ||
|+---------+| |+---------+|
+-----------+ +-----------+
A connection handshake allows, briefly, that the modules on two chains agree on a consistent view of their state (i.e., their store), and each module allocates a new connection associated with this state.
We identify two basic (high-level) requirements that a connection handshake should guarantee:
-
[ICS3-Seq-1-Agreement] If Alice finishes a handshake by opening a new connection
c1
with Bob, and Bob finishes the handshake opening the connectionc2
, thenc1 == c2
. In other words, if a connection handshake finishes, the two parties have a consistent view of this connection. We say that Alice stores one end of the connection, labelc1
, and Bob stores the other end of the same connection, labelc2
. -
[ICS3-Seq-2-Termination] Eventually, the modules of Alice and Bob should both finish the handshake, opening a connection. Opening a connection means that Alice and Bob both allocate in their local store a new connection.
Requirement [ICS3-Seq-1-Agreement] represents the basic safety guarantee, while [ICS3-Seq-2-Termination] is the basic liveness guarantee we would like to have.
A chain is a deterministic state machine. Each chain may be replicated, i.e., consisting of a replicated state machine (RSM), or it may be a standalone process. As a side note, there are certain requirements a chain must satisfy in practice(e.g., see [ICS 024]); at this level of specification, however, we model each chain as consisting of those three components we described earlier: module, store, and a client.
In ICS3, actors represent entities that may trigger this protocol and provide external feedback to the protocol. To quote from IBC terminology:
An actor, or a user (used interchangeably), is an entity interacting with the IBC protocol. An actor can be a human end-user, a module or smart contract running on a blockchain, or an off-chain relayer process capable of signing transactions. [IBC terminology]
A relayer intermediates communication between Alice and Bob. We model communication as a shared mutable state. Each chain exposes an API comprising read, write, as well as a queue (FIFO) functionality. So there are two parts to the communication API:
-
a read/write store:
- this holds the entire state of the chain;
- each module can write to this store, and we primarily concerned with the function
setConnection
that write a new connection into the store; - external processes -- in our case, any relayer -- can read from the store via a function
getConnection
;
-
a queue of datagrams, or messages:
- each module can
pop
datagrams stored in this queue; - external processes (relayers) can
push
to this queue.
- each module can
We describe the API in the code snippet below.
Connection handshake modules implement this interface.
Note that we use the modifiers 'private' and 'public' to denote which API function can be invoked by external processes (e.g., relayer) and which functions are private to the module implementing this interface.
The data types (Identifier
and ConnectionEnd
) are defined below.
type CommunicationLayer interface {
// Write a new value (a connection) to the local store, indexed by the
// identifier of the connection on the local party.
private setConnection(i Identifier, e ConnectionEnd)
// Relayer invokes this to `read` from a chains' local store.
public getConnection(i Identifier) -> ConnectionEnd
// Relayer invokes this to append a datagram into a chain's
// local store.
public push("outstandingDatagrams", g Datagram)
// A module consumes elements from its local store
// "outstandingDatagrams".
private pop("outstandingDatagrams") -> Datagram
// The typical use case is that for each datagram, this module invokes the
// corresponding handler, e.g., ConnTryHandler.
}
Preconditions:
- the datagram
g
as well as Identifieri
and Connectione
(respectively forpush
,setConnection
andgetConnection
) are non-null;
Postconditions:
- for
setConnection
: the local store on the module implementing this function stores at positioni
the valuee
; - for
push
: theoutstandingDatagrams
queue will append the Datagramg
Error conditions:
getConnection
may fail (returningnull
) if noConnectionEnd
exists for the input parameteri
;- before invoking the function
setConnection
, the caller should make sure that the client in theIdentifier
field was created;
Properties of the communication layer:
-
[ICS3-CommAssumption-1-RWIntegrity] If some connection
e
is returned fromgetConnection(i)
then thesetConnection(i, e)
was previously invoked. -
[ICS3-CommAssumption-2-QueueIntegrity] If some module
d
returns a datagramg
fromPop("outstandingDatagrams")
, then a process previously invokedPush("outstandingDatagrams", g)
. -
[ICS3-CommAssumption-3-QueueNoLoss] If some process invokes
Push("outstandingDatagrams", g)
, then eventually aPop
operation will returng
.
Refinement remarks (relation to L3):
- The
push
function is a more abstract version ofsubmitDatagram
(ICS 018). - The
setConnection
function is a more abstract version of the functionality accomplished byprovableStore.set
andaddConnectionToClient
(ICS 003). - The
getConnection
function is the analogy togetConnection
(ICS 018).
The chains Alice and Bob (and their module, local store, and client, respectively) may be subject to arbitrary (Byzantine) faults. The properties we define below are guaranteed to hold iff both Alice and Bob behave correctly, i.e., do not deviate from any protocol we describe below. Beside these two parties, the system comprises an arbitrary number of other parties, any of which may be Byzantine; these other chains may, for instance, try to impersonate Alice or Bob or attack them in some way. The termination property of the ICS3 protocol depends on the existence of at least one correct relayer. Additionally, there may be an arbitrary number of relayers that can communicate (i.e., read/push) to Alice and Bob and are subject to Byzantine faults.
We explicitly overlook here two assumptions of synchrony that may be necessary for achieving liveness of other protocols, but they are not necessary within the ICS3 protocol.
These assumptions are: (i) partial synchrony is necessary for achieving liveness of a chain if that chain is implemented using a deterministic consensus protocol (e.g., Tendermint Core); (ii) synchrony is necessary for guaranteeing message delivery in any connection-oriented protocol such as TCP, which is most likely the underlying network transport layer (implementing the CommunicationLayer
between modules and relayers).
We introduce several abstractions that are relevant at this point:
- A
ConnectionParty
is a data type encapsulating the details that identify a party.
type ConnectionParty interface {
connectionIdentifier Identifier
clientIdentifier Identifier
prefix Prefix
}
- A
ConnectionState
captures the state of a connection, and may have one of the following values.
enum ConnectionState {
UNINIT,
INIT,
TRYOPEN,
OPEN,
}
- A
ConnectionEnd
is a data type that captures all the details of a connection at a party. This includes a remote and a local field, so that the local party is the one storing this object, and the remote party is the other one. One of the goals of the connection handshake protocol is to allocate an object of this type at each of Alice and Bob.
type ConnectionEnd interface {
state ConnectionState
remoteParty ConnectionParty
localParty ConnectionParty
version []String
}
- The
ConnProof
andClientProof
are two data types that abstracts over the details of a cryptographic proof that any module can create, and another module can verify. Intuitively, a proof is helpful for some module to guarantee that it is in a certain state. We are concerned with connection proofs (ConnProof
type) and client proofs (ClientProof
) here.
type ConnProof struct {
proof CommitmentProof // The proof for the connection that the remote party claims to have.
}
type ClientProof struct {
proof CommitmentProof // The proof for the client that the remote party claims to have.
height uint64 // The height which the client (on remote party) claims having.
}
We now restate the connection handshake problem in a slightly more precise (or lower level) formulation:
A connection handshake enables two ICS3 modules to agree on a consistent view of their chain state, to verify each other's state, and to allocate a new connection.
We refine the safety guarantee [ICS3-Seq-1-Agreement] (defined in the sequential problem statement) by splitting this into three complementary safety properties:
-
[ICS3-Proto-1-ConnectionUniqueness] A module accepts (i.e., initializes on) a
ConnectionEnd
e
at most once. -
[ICS3-Proto-2-ConnectionIntegrity] If any two modules open a connection
e
, then either one module or the other or both modules accepted (i.e., initialized with)e
. -
[ICS3-Proto-3-StateConsistency] If any two modules open a connection, then the client in the first module is consistent with the state of the second module.
The liveness property below is equivalent to [ICS3-Seq-2-Termination] (the livenes property in the sequential specification above):
- [ICS3-Proto-4-Termination]
If the two parties involved in a connection handshake are correct, then the connection handshake eventually terminates.
Specifically, termination implies that each module allocates in the local store a new
ConnectionEnd
object withstate
field set toOPEN
.
Remarks:
-
Uniqueness property essentially provides a safeguard against overwriting a connection in the store with some new set of parameters.
-
The integrity property, in conjunction with uniqueness, ensures that there is continuity between the connections that a module initializes and the connections that this module opens.
-
One of the mechanisms that enforces the connection integrity property consists of cryptographic proofs, specifically the
ConnProof
type, which asserts that a certain chain stores a certain connection in a certain state. -
The
ClientProof
type guarantees the state consistency property, by asserting that the client on a chain is updated to a consensus state of the remote chain. -
Note that the consistency property works both ways. This property applies to any two modules (there is no predefined "first" or "second" module here).
The ICS3 protocol comprises four steps, summarized below.
-
An actor invokes the
ConnInitHandler
handler at the ICS3 module in one of the chains; this sets off the connection handshake protocol. In our example, we will use Alice as the party to executeConnInitHandler
. Once Alice does so, this handler marks the connection on her end as initialized (i.e.,INIT
). -
This comprises two sub-steps:
- 2.a. Upon observing that Alice has executed
ConnInitHandler
(meaning that her state contains a connection that isINIT
), the relayer constructs a datagram of typeConnOpenTry
and pushes this at Bob's module; - 2.b. Bob handles this datagram via the
ConnTryHandler
handler. Once this handler finishes, the connection on his end is marked asTRYOPEN
.
- 2.a. Upon observing that Alice has executed
-
This comprises two sub-steps:
- 3.a. Upon observing that Bob has finished executing
ConnTryHandler
(i.e., his state contains aTRYOPEN
connection), the relayer constructs a datagram of typeConnOpenAck
and pushes this at Alice's module; - 3.b. Alice handles this datagram by invoking the
ConnAckHandler
handler. Once this handler finishes, Alice considers herConnectionEnd
in stateOPEN
.
- 3.a. Upon observing that Bob has finished executing
-
This comprises two sub-steps:
- 4.a. When the relayer observes that Alice finished handling the
ConnOpenAck
datagram (and consequently the connection is open on her side), the relayer constructs aConnOpenConfirm
datagram and pushes it on Bob's side. - 4.b. Finally, Bob's module processes the
ConnOpenConfirm
datagram through an eponymous handler, which sets the connection state toOPEN
for him and thereby marks the termination of the ICS3 protocol.
- 4.a. When the relayer observes that Alice finished handling the
The diagram below sketches these fours steps of handshake protocol.
The four steps comprising
IBC Connection Handshake Protocol
Alice Bob
+-----------+ +-----------+
|ICS3 Module| |ICS3 Module|
| | | |
| step 1 | |
| ConnInitHandler | |
| ►INIT | | |
| |<-2.a--Relayer-----2.a-->| |
| | | | |
| | \-> step 2.b |
| | ConnTryHandler |
| | | ►TRYOPEN |
| <--3.a------Relayer--3.a->| |
| | | | |
| step 3.b <---/ | |
| ConnAckHandler | |
| ►OPEN | | |
| |<-4.a---Relayer----4.a-->| |
| | | | |
| | \-> step 4.b |
| | ConnConfirmHandler |
| | | ►OPEN |
+-----------+ +-----------+
We first present the signatures of the four protocol handlers; the connection handshake module at each party implements and exposes these handlers.
func ConnInitHandler(
local ConnectionParty,
remote ConnectionParty)
func ConnTryHandler(
local ConnectionParty,
remote ConnectionParty,
remoteVersions []String,
proofsHeight uint64,
remoteConnectionProof ConnProof,
remoteClientProof ClientProof)
func ConnAckHandler(
local ConnectionParty,
remoteVersion String,
proofsHeight uint64,
remoteConnectionProof ConnProof,
remoteClientProof ClientProof)
func ConnConfirmHandler(
local ConnectionParty,
proofsHeight uint64,
remoteConnectionProof ConnProof)
We first define the four protocol handlers, and then describe the datagrams. Finally, we discuss some helper functions.
This is step 1.
func ConnInitHandler(local ConnectionParty, remote ConnectionParty)
{
// Create local end of the connection in the state 'INIT'.
connectionEnd = newConnectionEnd('INIT', local, remote, getCompatibleVersions())
// Uniqueness check.
// The Init handler for a given connection may run only once.
// Abort, unless this is the first time initializing this connection.
abortTransactionUnless(getConnection(local.connectionIdentifier) == nil)
// Now save this connection end in the local store.
// This connectionEnd is in state INIT, the relayer will continue with
// next steps of the connection handshake protocol.
setConnection(local.connectionIdentifier, connectionEnd)
}
Preconditions:
- The parameters
local
andremote
should be valid, i.e., a syntactically correct (see validation). - This handler must not have executed previously with the same
local
input parameter (in particular, the samelocal.connectionIdentifier
field).
Postconditions:
- The module executing this handler stores a
ConnectionEnd
(matching with the input argumentslocal
andremote
) in stateINIT
. - Uniqueness is satisfied because this handler aborts if a key with the identifier
local.connectionIdentifier
already exists in the store (at-most-once semantics). - To ensure termination of the ICS3 protocol, this handler must execute either on one of the chains or both (at-least-once semantics).
- Handler produces no output; may abort if preconditions not met.
This is step 2.b.
func ConnTryHandler(
local ConnectionParty,
remote ConnectionParty,
remoteVersions []String,
proofsHeight uint64,
remoteConnectionProof ConnProof,
remoteClientProof ClientProof)
{
// Create local end of the connection.
connectionEnd = newConnectionEnd('TRYOPEN', local, remote, pickVersion(remoteVersions))
// Uniqueness check.
// Verify that if there is a connection then the Init handler previously executed
// with these same parameters).
current = getConnection(local.connectionIdentifier)
abortTransactionUnless(
current == nil ||
current.state == 'INIT' && matchingEnds(connectionEnd, current))
// Verify proofs.
expectedRemoteConnection = newConnectionEnd('INIT', remote, local, remoteVersions)
abortTransactionUnless(verifyProofs(
expectedRemoteConnection, proofsHeight, remoteConnectionProof, remoteClientProof)
// Handler done, store the updated connectionEnd.
setConnection(local.connectionIdentifier, connectionEnd)
}
Preconditions:
- The input parameters should be valid; among others validation criteria, it is important that the module running this handler supports at least one of the versions supplied in the input list
remoteVersions
(see validation). - The two proofs
remoteConnectionProof
andremoteClientProof
should be correct. This is necessary in connection to properties [ICS3-Proto-2-ConnectionIntegrity] and [ICS3-Proto-3-StateConsistency]. Correctness of proofs means that they pass verification (verifyProof
function).
Postconditions:
- The module executing this handler stores a
ConnectionEnd
(matching with the argumentslocal
andremote
) in stateTRYOPEN
. - No output; may abort if preconditions not met.
Step 3.b.
func ConnAckHandler(
local ConnectionParty,
remoteVersion String,
proofsHeight uint64,
remoteConnectionProof ConnProof,
remoteClientProof ClientProof)
{
// Search the details of this connection in local store.
connectionEnd = getConnection(local.connectionIdentifier)
// This verification helps guarantee uniqueness and integrity.
abortTransactionUnless(connectionEnd != nil && connectionEnd.local == local)
// Should not overwrite a connection that is already `OPEN`.
abortTransactionUnless(connectionEnd.state == INIT || connectionEnd.state == TRYOPEN)
// Verify proofs.
expectedRemoteConnection = newConnectionEnd('TRYOPEN', remote, local, remoteVersion)
abortTransactionUnless(verifyProofs(
expectedRemoteConnection, proofsHeight, remoteConnectionProof, remoteClientProof)
// Termination (partial -- only at this end)
connectionEnd.state = OPEN
connectionEnd.version = remoteVersion
// Update the connectionEnd in local state.
setConnection(local.connectionIdentifier, connectionEnd)
}
Preconditions:
- The module executing this handler already has a
ConnectionEnd
stored locally, matching thelocal
argument, and in stateINIT
orTRYOPEN
. - The input parameters are valid (see validation).
- The two proofs
remoteConnectionProof
andremoteClientProof
are correct; for guaranteeing properties [ICS3-Proto-2-ConnectionIntegrity] and [ICS3-Proto-3-StateConsistency].
Postconditions:
- The module executing this handler stores a
ConnectionEnd
(matching with the argumentslocal
) in stateOPEN
. - No output; may abort if preconditions not met.
Step 4.b.
func ConnConfirmHandler(
local ConnectionParty,
proofsHeight uint64,
remoteConnectionProof ConnProof)
{
// Search the details of this connection in the local store.
connectionEnd = getConnection(local.connectionIdentifier)
// Helps guarantee integrity and uniqueness.
abortTransactionUnless(connectionEnd != nil && connectionEnd.local == local)
// Integrity check: the handler should not overwrite a connection that is
// already OPEN. And should not be working with a connection that is
// INIT. The only acceptable state is TRYOPEN.
abortTransactionUnless(connectionEnd.state == TRYOPEN)
// Connection proof verification.
expectedRemoteConnection = newConnectionEnd('OPEN', remote, local, connectionEnd.version)
abortTransactionUnless(verifyProofs(
expectedRemoteConnection, proofsHeight, remoteConnectionProof, nil)
// Termination.
connectionEnd.state = OPEN
setConnection(local.connectionIdentifier, connectionEnd)
}
Preconditions:
- The module executing this handler has a
ConnectionEnd
stored locally, (matching the input argumentlocal
) and in stateTRYOPEN
. - The input parameters are valid (see validation).
- The input proof
remoteConnectionProof
is correct; for guaranteeing property [ICS3-Proto-2-ConnectionIntegrity].
Postconditions:
- The module executing this handler stores a
ConnectionEnd
(matching with the input argumentlocal
) in stateOPEN
. - No output; may abort if preconditions not met.
A correct relayer can push the following datagrams at a chain.
Upon popping a datagram of a certain type, e.g., ConnOpenConfirm
, a correct chain handles the datagram by first validating the datagram (syntactic validation, e.g., well-formed parameters, see validation) and then invoking the corresponding handler.
type ConnOpenInit struct {
local ConnectionParty
remote ConnectionParty
}
type ConnOpenTry struct {
local ConnectionParty
remote ConnectionParty
remoteVersions []String
proofsHeight uint64
remoteConnectionProof ConnProof
remoteClientProof ClientProof
}
type ConnOpenAck struct {
local ConnectionParty
remoteVersion String
proofsHeight uint64
remoteConnectionProof ConnProof
remoteClientProof ClientProof
}
type ConnOpenConfirm struct {
local ConnectionParty
proofsHeight uint64
remoteConnectionProof ConnProof
}
Notice that the fields in each datagram match with the corresponding protocol handler signature.
Checks that the connection ends in the local and remote arguments match. (NB: This does not check the connection state, only the connection parties).
When we talk of validation we usually mean a verification that is done before a handler executes, checking that the input parameters of that handler are syntactically correct, e.g., valid local
connection party, non-null proofs, non-zero proofs height.
With respect to the local
parameter, similar in functionality with validateConnectionIdentifier
from ICS 003, we also validate the local.connectionIdentifier
field.
Additionally, if a version
or remoteVersion
input parameter is present, this should also be validated prior to invoking a handler with that version.
This is identical with the one in ICS 003.
This is identical with the one in ICS 003.
This function handles everything related to proof verification.
It requires four arguments: the connection end as it expected to be on the remote party, the height where the proofs were taken, a connection proof, and a client proof; the last argument may be nil.
This function invokes a lower-level method client.verifyProof
, which executes from a client context and handles the cryptographic verification of a given proof; this method is analogous to verifyMembership
of the ICS 023, except it must additionally get the commitment root from the consensuss state and perform serialization on the expected data.
The pseudocode for the verifyProofs
is provided below.
func verifyProofs(
expectedConnection ConnectionEnd,
proofsHeight,
remoteConnectionProof ConnProof,
remoteClientProof ClientProof)
{
// Proofs verification.
// Generic proof check: Local client should exists and be updated with the
// height which the two proofs target (proofsHeight).
client = clientState(expectedConnection.remote.clientIdentifier)
abortTransactionUnless(client != nil)
clientConsensusState = clientConsensusState(expectedConnection.remote.clientIdentifier), proofsHeight)
abortTransactionUnless(clientConsensusState != nil)
// Connection proof specific verification.
abortTransactionUnless(client.verifyProof(
clientConsensusState, remoteConnectionProof,
connectionPath(local.connectionIdentifier), expectedConnection))
if remoteClientProof == nil {
return // Client proof is missing, so skip any further verification.
}
// Client proof verification ensures state consistency: the remote
// client should be consistent with the state of local party.
expectedRemoteClientConsensus = localConsensusState(remoteClientProof.height)
abortTransactionUnless(remoteClientProof.height <= getCurrentHeight())
abortTransactionUnless(client.verifyProof(
clientConsensusState, remoteClientProof,
clientPath(local.clientIdentifier, remoteClientProof.height),
expectedRemoteClientConsensus))
}
These are analogous to getConsensusState
and queryClient
functions.
The clientConsensusState
, in particular, has an implicit requirement that it may not return the consensus state of a client if the height is older than a certain threshold; the proofsHeight
should not be outdated by more than 100
heights, specifically. (This means that proofs must not be too old.)
-
Standard cryptographic assumptions (collision-resistant hash functions, public-key signatures). We should mention these once we have more details about the implementation.
-
How to capture aborts or incorrect termination? See issue raised by Anca. Is this necessary? To inform this discussion, more implementation details are needed.
-
Verification of the unbonding period in
ConnTryHandler
. See ICS/#403. -
Missing link to L3: what is the mechanism that implements the
pop
functionality at the implementation/L3 level (hint: it's in the SDK, the layer sitting between the consensus module and IBC Handler).
- [ICS 003] Interchain Standards ICS 003 Connection Semantics.
- [ICS 024] Interchain Standards ICS 024 Host Requirements.
- [ICS 018] Interchain Standards ICS 024 Host Requirements.