ics | title | stage | category | kind | requires | author | created | modified |
---|---|---|---|---|---|---|---|---|
18 |
Relayer Algorithms |
draft |
IBC/TAO |
interface |
24, 25, 26 |
Christopher Goes <[email protected]> |
2019-03-07 |
2019-08-25 |
Relayer algorithms are the "physical" connection layer of IBC — off-chain processes responsible for relaying data between two chains running the IBC protocol by scanning the state of each chain, constructing appropriate datagrams, and executing them on the opposite chain as allowed by the protocol.
In the IBC protocol, a blockchain can only record the intention to send particular data to another chain — it does not have direct access to a network transport layer. Physical datagram relay must be performed by off-chain infrastructure with access to a transport layer such as TCP/IP. This standard defines the concept of a relayer algorithm, executable by an off-chain process with the ability to query chain state, to perform this relay.
A relayer is an off-chain process with the ability to read the state of and submit transactions to some set of ledgers utilising the IBC protocol.
- No exactly-once or deliver-or-timeout safety properties of IBC should depend on relayer behaviour (assume Byzantine relayers).
- Packet relay liveness properties of IBC should depend only on the existence of at least one correct, live relayer.
- Relaying should be permissionless, all requisite verification should be performed on-chain.
- Requisite communication between the IBC user and the relayer should be minimised.
- Provision for relayer incentivisation should be possible at the application layer.
The relayer algorithm is defined over a set C
of chains implementing the IBC protocol. Each relayer may not necessarily have access to read state from and write datagrams to all chains in the interchain network (especially in the case of permissioned or private chains) — different relayers may relay between different subsets.
pendingDatagrams
calculates the set of all valid datagrams to be relayed from one chain to another based on the state of both chains. The relayer must possess prior knowledge of what subset of the IBC protocol is implemented by the blockchains in the set for which they are relaying (e.g. by reading the source code). An example is defined below.
submitDatagram
is a procedure defined per-chain (submitting a transaction of some sort). Datagrams can be submitted individually as single transactions or atomically as a single transaction if the chain supports it.
relay
is called by the relayer every so often — no more frequently than once per block on either chain, and possibly less frequently, according to how often the relayer wishes to relay.
Different relayers may relay between different chains — as long as each pair of chains has at least one correct & live relayer and the chains remain live, all packets flowing between chains in the network will eventually be relayed.
function relay(C: Set<Chain>) {
for (const chain of C)
for (const counterparty of C)
if (counterparty !== chain) {
const datagrams = chain.pendingDatagrams(counterparty)
for (const localDatagram of datagrams[0])
chain.submitDatagram(localDatagram)
for (const counterpartyDatagram of datagrams[1])
counterparty.submitDatagram(counterpartyDatagram)
}
}
Packets in an ordered channel can be relayed in either an event-based fashion or a query-based fashion. For the former, the relayer should watch the source chain for events emitted whenever packets are sent, then compose the packet using the data in the event log. For the latter, the relayer should periodically query the send sequence on the source chain, and keep the last sequence number relayed, so that any sequences in between the two are packets that need to be queried & then relayed. In either case, subsequently, the relayer process should check that the destination chain has not yet received the packet by checking the receive sequence, and then relay it.
Packets in an unordered channel can be relayed in an event-based fashion. The relayer should watch the source chain for events emitted whenever packets are sent, then compose the packet using the data in the event log. Subsequently, the relayer should check whether the destination chain has received the packet already by querying for the presence of an acknowledgement at the packet's sequence number, and if one is not yet present the relayer should relay the packet.
Acknowledgements can be relayed in an event-based fashion. The relayer should watch the destination chain for events emitted whenever packets are received & acknowledgements are written, then compose the acknowledgement using the data in the event log, check whether the packet commitment still exists on the source chain (it will be deleted once the acknowledgement is relayed), and if so relay the acknowledgement to the source chain.
Timeout relay is slightly more complex since there is no specific event emitted when a packet times-out - it is simply the case that the packet can no longer be relayed, since the timeout height or timestamp has passed on the destination chain. The relayer process must elect to track a set of packets (which can be constructed by scanning event logs), and as soon as the height or timestamp of the destination chain exceeds that of a tracked packet, check whether the packet commitment still exists on the source chain (it will be deleted once the timeout is relayed), and if so relay a timeout to the source chain.
Some channel types (e.g. ORDERED_ALLOW_TIMEOUT), can only timeout a packet if a timeout receipt is written on the destination chain. This requires a relayer to first attempt a receive on the destination chain even if the packet is already timed out, before they can relay a timeout to the sending chain. Thus on these channels, relayers must check if packet has already been received on the destination chain by querying the packet receipt path. If a value does not already exist, then attempt to receive the packet on the destination chain. If a timeout receipt is written, then relay the timeout with a proof of the timeout receipt back to the sender chain.
pendingDatagrams
collates datagrams to be sent from one machine to another. The implementation of this function will depend on the subset of the IBC protocol supported by both machines & the state layout of the source machine. Particular relayers will likely also want to implement their own filter functions in order to relay only a subset of the datagrams which could possibly be relayed (e.g. the subset for which they have been paid to relay in some off-chain manner).
An example implementation which performs unidirectional relay between two chains is outlined below. It can be altered to perform bidirectional relay by switching chain
and counterparty
.
Which relayer process is responsible for which datagrams is a flexible choice - in this example, the relayer process relays all handshakes which started on chain
(sending datagrams to both chains), relays all packets sent from chain
to counterparty
, and relays all acknowledgements of packets sent from counterparty
to chain
.
function pendingDatagrams(chain: Chain, counterparty: Chain): List<Set<Datagram>> {
const localDatagrams = []
const counterpartyDatagrams = []
// ICS2 : Clients
// - Determine if light client needs to be updated (local & counterparty)
height = chain.latestHeight()
client = counterparty.queryClientConsensusState(chain)
if client.height < height {
header = chain.latestHeader()
counterpartyDatagrams.push(ClientUpdate{chain, header})
}
counterpartyHeight = counterparty.latestHeight()
client = chain.queryClientConsensusState(counterparty)
if client.height < counterpartyHeight {
header = counterparty.latestHeader()
localDatagrams.push(ClientUpdate{counterparty, header})
}
// ICS3 : Connections
// - Determine if any connection handshakes are in progress
connections = chain.getConnectionsUsingClient(counterparty)
for (const localEnd of connections) {
remoteEnd = counterparty.getConnection(localEnd.counterpartyIdentifier)
if (localEnd.state === INIT && remoteEnd === null)
// Handshake has started locally (1 step done), relay `connOpenTry` to the remote end
counterpartyDatagrams.push(ConnOpenTry{
desiredIdentifier: localEnd.counterpartyConnectionIdentifier,
counterpartyConnectionIdentifier: localEnd.identifier,
counterpartyClientIdentifier: localEnd.clientIdentifier,
counterpartyPrefix: localEnd.commitmentPrefix,
clientIdentifier: localEnd.counterpartyClientIdentifier,
version: localEnd.version,
counterpartyVersion: localEnd.version,
proofInit: localEnd.proof(height),
proofConsensus: localEnd.client.consensusState.proof(),
proofHeight: height,
consensusHeight: localEnd.client.height,
})
else if (localEnd.state === INIT && remoteEnd.state === TRYOPEN)
// Handshake has started on the other end (2 steps done), relay `connOpenAck` to the local end
localDatagrams.push(ConnOpenAck{
identifier: localEnd.identifier,
version: remoteEnd.version,
proofTry: remoteEnd.proof(counterpartyHeight),
proofConsensus: remoteEnd.client.consensusState.proof(),
proofHeight: counterpartyHeight,
consensusHeight: remoteEnd.client.height,
})
else if (localEnd.state === OPEN && remoteEnd.state === TRYOPEN)
// Handshake has confirmed locally (3 steps done), relay `connOpenConfirm` to the remote end
counterpartyDatagrams.push(ConnOpenConfirm{
identifier: remoteEnd.identifier,
proofAck: localEnd.proof(height),
proofHeight: height,
})
}
// ICS4 : Channels & Packets
// - Determine if any channel handshakes are in progress
// - Determine if any packets, acknowledgements, or timeouts need to be relayed
channels = chain.getChannelsUsingConnections(connections)
for (const localEnd of channels) {
remoteEnd = counterparty.getConnection(localEnd.counterpartyIdentifier)
// Deal with handshakes in progress
if (localEnd.state === INIT && remoteEnd === null)
// Handshake has started locally (1 step done), relay `chanOpenTry` to the remote end
counterpartyDatagrams.push(ChanOpenTry{
order: localEnd.order,
connectionHops: localEnd.connectionHops.reverse(),
portIdentifier: localEnd.counterpartyPortIdentifier,
channelIdentifier: localEnd.counterpartyChannelIdentifier,
counterpartyPortIdentifier: localEnd.portIdentifier,
counterpartyChannelIdentifier: localEnd.channelIdentifier,
version: localEnd.version,
counterpartyVersion: localEnd.version,
proofInit: localEnd.proof(height),
proofHeight: height,
})
else if (localEnd.state === INIT && remoteEnd.state === TRYOPEN)
// Handshake has started on the other end (2 steps done), relay `chanOpenAck` to the local end
localDatagrams.push(ChanOpenAck{
portIdentifier: localEnd.portIdentifier,
channelIdentifier: localEnd.channelIdentifier,
version: remoteEnd.version,
proofTry: remoteEnd.proof(counterpartyHeight),
proofHeight: counterpartyHeight,
})
else if (localEnd.state === OPEN && remoteEnd.state === TRYOPEN)
// Handshake has confirmed locally (3 steps done), relay `chanOpenConfirm` to the remote end
counterpartyDatagrams.push(ChanOpenConfirm{
portIdentifier: remoteEnd.portIdentifier,
channelIdentifier: remoteEnd.channelIdentifier,
proofAck: localEnd.proof(height),
proofHeight: height
})
// Deal with packets
// First, scan logs for sent packets and relay all of them
sentPacketLogs = queryByTopic(height, "sendPacket")
for (const logEntry of sentPacketLogs) {
// relay packet with this sequence number
packetData = Packet{logEntry.sequence, logEntry.timeoutHeight, logEntry.timeoutTimestamp,
localEnd.portIdentifier, localEnd.channelIdentifier,
remoteEnd.portIdentifier, remoteEnd.channelIdentifier, logEntry.data}
counterpartyDatagrams.push(PacketRecv{
packet: packetData,
proof: packet.proof(),
proofHeight: height,
})
}
// Then, scan logs for acknowledgements, relay back to sending chain
recvPacketLogs = queryByTopic(height, "writeAcknowledgement")
for (const logEntry of recvPacketLogs) {
// relay packet acknowledgement with this sequence number
packetData = Packet{logEntry.sequence, logEntry.timeoutHeight, logEntry.timeoutTimestamp,
localEnd.portIdentifier, localEnd.channelIdentifier,
remoteEnd.portIdentifier, remoteEnd.channelIdentifier, logEntry.data}
counterpartyDatagrams.push(PacketAcknowledgement{
packet: packetData,
acknowledgement: logEntry.acknowledgement,
proof: packet.proof(),
proofHeight: height,
})
}
}
return [localDatagrams, counterpartyDatagrams]
}
Relayers may elect to filter these datagrams in order to relay particular clients, particular connections, particular channels, or even particular kinds of packets, perhaps in accordance with the fee payment model (which this document does not specify, as it may vary).
There are implicit ordering constraints imposed on the relayer process determining which datagrams must be submitted in what order. For example, a header must be submitted to finalise the stored consensus state & commitment root for a particular height in a light client before a packet can be relayed. The relayer process is responsible for frequently querying the state of the chains between which they are relaying in order to determine what must be relayed when.
If the host state machine supports it, the relayer process can bundle many datagrams into a single transaction, which will cause them to be executed in sequence, and amortise any overhead costs (e.g. signature checks for fee payment).
Multiple relayers relaying between the same pair of modules & chains may attempt to relay the same packet (or submit the same header) at the same time. If two relayers do so, the first transaction will succeed and the second will fail. Out-of-band coordination between the relayers or between the actors who sent the original packets and the relayers is necessary to mitigate this. Further discussion is out of scope of this standard.
The relay process must have access to accounts on both chains with sufficient balance to pay for transaction fees. Relayers may employ application-level methods to recoup these fees, such as by including a small payment to themselves in the packet data — protocols for relayer fee payment will be described in future versions of this ICS or in separate ICSs.
Any number of relayer processes may be safely run in parallel (and indeed, it is expected that separate relayers will serve separate subsets of the interchain). However, they may consume unnecessary fees if they submit the same proof multiple times, so some minimal coordination may be ideal (such as assigning particular relayers to particular packets or scanning mempools for pending transactions).
Not applicable. The relayer process is off-chain and can be upgraded or downgraded as necessary.
Not applicable. The relayer process is off-chain and can be upgraded or downgraded as necessary.
- Implementation of ICS 18 in Go can be found in cosmos/relayer repository.
- Implementation of ICS 18 in Rust can be found in informalsystems/hermes repository.
Mar 30, 2019 - Initial draft submitted
Apr 15, 2019 - Revisions for formatting and clarity
Apr 23, 2019 - Revisions from comments; draft merged
All content herein is licensed under Apache 2.0.