Skip to content

Latest commit

 

History

History

ics-026-routing-module

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
ics title stage category kind version compatibility author created modified
26
Routing Module
Draft
IBC/TAO
instantiation
ibc-go v7.0.0
Christopher Goes <cwgoes@tendermint.com>
2019-06-09
2019-08-25

Synopsis

The routing module is a default implementation of a secondary module which will accept external datagrams and call into the interblockchain communication protocol handler to deal with handshakes and packet relay. The routing module keeps a lookup table of modules, which it can use to look up and call a module when a packet is received, so that external relayers need only ever relay packets to the routing module.

Motivation

The default IBC handler uses a receiver call pattern, where modules must individually call the IBC handler in order to bind to ports, start handshakes, accept handshakes, send and receive packets, etc. This is flexible and simple but is a bit tricky to understand and may require extra work on the part of relayer processes, who must track the state of many modules. This standard describes an IBC "routing module" to automate most common functionality, route packets, and simplify the task of relayers.

The routing module can also play the role of the module manager as discussed in ICS 5 and implement logic to determine when modules are allowed to bind to ports and what those ports can be named.

Definitions

All functions provided by the IBC handler interface are defined as in ICS 25.

The functions newCapability & authenticateCapability are defined as in ICS 5.

The functions writeChannel and writeAcknowledgement are defined as in ICS 4

Desired Properties

  • Modules should be able to bind to ports and own channels through the routing module.
  • No overhead should be added for packet sends and receives other than the layer of call indirection.
  • The routing module should call specified handler functions on modules when they need to act upon packets.

Technical Specification

Note: If the host state machine is utilising object capability authentication (see ICS 005), all functions utilising ports take an additional capability parameter.

Module callback interface

Modules must expose the following function signatures to the routing module, which are called upon the receipt of various datagrams:

OnChanOpenInit

onChanOpenInit will verify that the relayer-chosen parameters are valid and perform any custom INIT logic. It may return an error if the chosen parameters are invalid in which case the handshake is aborted. If the provided version string is non-empty, onChanOpenInit should return the version string or an error if the provided version is invalid. If the version string is empty, onChanOpenInit is expected to return a default version string representing the version(s) it supports. If there is no default version string for the application, it should return an error if provided version is empty string.

function onChanOpenInit(
  order: ChannelOrder,
  connectionHops: [Identifier],
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyPortIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  version: string) => (version: string, err: Error) {
    // defined by the module
}

OnChanOpenTry

onChanOpenTry will verify the INIT-chosen parameters along with the counterparty-chosen version string and perform custom TRY logic. If the INIT-chosen parameters are invalid, the callback must return an error to abort the handshake. If the counterparty-chosen version is not compatible with this modules supported versions, the callback must return an error to abort the handshake. If the versions are compatible, the try callback must select the final version string and return it to core IBC. onChanOpenTry may also perform custom initialization logic

function onChanOpenTry(
  order: ChannelOrder,
  connectionHops: [Identifier],
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyPortIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  counterpartyVersion: string) => (version: string, err: Error) {
    // defined by the module
}

OnChanOpenAck

onChanOpenAck will error if the counterparty selected version string is invalid to abort the handshake. It may also perform custom ACK logic.

function onChanOpenAck(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier, 
  counterpartyVersion: string) {
    // defined by the module
}

OnChanOpenConfirm

onChanOpenConfirm will perform custom CONFIRM logic and may error to abort the handshake.

function onChanOpenConfirm(
  portIdentifier: Identifier,
  channelIdentifier: Identifier) {
    // defined by the module
}
function onChanCloseInit(
  portIdentifier: Identifier,
  channelIdentifier: Identifier) {
    // defined by the module
}

function onChanCloseConfirm(
  portIdentifier: Identifier,
  channelIdentifier: Identifier): void {
    // defined by the module
}

function onRecvPacket(packet: Packet, relayer: string): bytes {
    // defined by the module, returns acknowledgement
}

function onTimeoutPacket(packet: Packet, relayer: string) {
    // defined by the module
}

function onAcknowledgePacket(packet: Packet, acknowledgement: bytes, relayer: string) {
    // defined by the module
}

function onTimeoutPacketClose(packet: Packet, relayer: string) {
    // defined by the module
}

Exceptions MUST be thrown to indicate failure and reject the handshake, incoming packet, etc.

These are combined together in a ModuleCallbacks interface:

interface ModuleCallbacks {
  onChanOpenInit: onChanOpenInit
  onChanOpenTry: onChanOpenTry
  onChanOpenAck: onChanOpenAck
  onChanOpenConfirm: onChanOpenConfirm
  onChanCloseInit: onChanCloseInit
  onChanCloseConfirm: onChanCloseConfirm
  onRecvPacket: onRecvPacket
  onTimeoutPacket: onTimeoutPacket
  onAcknowledgePacket: onAcknowledgePacket
  onTimeoutPacketClose: onTimeoutPacketClose
}

Callbacks are provided when the module binds to a port.

function callbackPath(portIdentifier: Identifier): Path {
    return "callbacks/{portIdentifier}"
}

The calling module identifier is also stored for future authentication should the callbacks need to be altered.

function authenticationPath(portIdentifier: Identifier): Path {
    return "authentication/{portIdentifier}"
}

Port binding as module manager

The IBC routing module sits in-between the handler module (ICS 25) and individual modules on the host state machine.

The routing module, acting as a module manager, differentiates between two kinds of ports:

  • "Existing name” ports: e.g. “bank”, with standardised prior meanings, which should not be first-come-first-serve
  • “Fresh name” ports: new identity (perhaps a smart contract) w/no prior relationships, new random number port, post-generation port name can be communicated over another channel

A set of existing names are allocated, along with corresponding modules, when the routing module is instantiated by the host state machine. The routing module then allows allocation of fresh ports at any time by modules, but they must use a specific standardised prefix.

The function bindPort can be called by a module in order to bind to a port, through the routing module, and set up callbacks.

function bindPort(
  id: Identifier,
  callbacks: Callbacks): CapabilityKey {
    abortTransactionUnless(privateStore.get(callbackPath(id)) === null)
    privateStore.set(callbackPath(id), callbacks)
    capability = handler.bindPort(id)
    claimCapability(authenticationPath(id), capability)
    return capability
}

The function updatePort can be called by a module in order to alter the callbacks.

function updatePort(
  id: Identifier,
  capability: CapabilityKey,
  newCallbacks: Callbacks) {
    abortTransactionUnless(authenticateCapability(authenticationPath(id), capability))
    privateStore.set(callbackPath(id), newCallbacks)
}

The function releasePort can be called by a module in order to release a port previously in use.

Warning: releasing a port will allow other modules to bind to that port and possibly intercept incoming channel opening handshakes. Modules should release ports only when doing so is safe.

function releasePort(
  id: Identifier,
  capability: CapabilityKey) {
    abortTransactionUnless(authenticateCapability(authenticationPath(id), capability))
    handler.releasePort(id)
    privateStore.delete(callbackPath(id))
    privateStore.delete(authenticationPath(id))
}

The function lookupModule can be used by the routing module to lookup the callbacks bound to a particular port.

function lookupModule(portId: Identifier) {
    return privateStore.get(callbackPath(portId))
}

Datagram handlers (write)

Datagrams are external data blobs accepted as transactions by the routing module. This section defines a handler function for each datagram, which is executed when the associated datagram is submitted to the routing module in a transaction.

All datagrams can also be safely submitted by other modules to the routing module.

No message signatures or data validity checks are assumed beyond those which are explicitly indicated.

Client lifecycle management

ClientCreate creates a new light client with the specified identifier & consensus state.

interface ClientCreate {
  identifier: Identifier
  clientState: ClientState
  consensusState: ConsensusState
}
function handleClientCreate(datagram: ClientCreate) {
    handler.createClient(datagram.clientState, datagram.consensusState)
}

ClientUpdate updates an existing light client with the specified identifier & new header.

interface ClientUpdate {
  identifier: Identifier
  header: Header
}
function handleClientUpdate(datagram: ClientUpdate) {
    handler.updateClient(datagram.identifier, datagram.header)
}

ClientSubmitMisbehaviour submits proof-of-misbehaviour to an existing light client with the specified identifier.

interface ClientMisbehaviour {
  identifier: Identifier
  evidence: bytes
}
function handleClientMisbehaviour(datagram: ClientMisbehaviour) {
    handler.submitMisbehaviourToClient(datagram.identifier, datagram.evidence)
}

Connection lifecycle management

The ConnOpenInit datagram starts the connection handshake process with an IBC module on another chain.

interface ConnOpenInit {
  counterpartyPrefix: CommitmentPrefix
  clientIdentifier: Identifier
  counterpartyClientIdentifier: Identifier
  version: string
  delayPeriodTime: uint64
  delayPeriodBlocks: uint64
}
function handleConnOpenInit(datagram: ConnOpenInit) {
  handler.connOpenInit(
    datagram.counterpartyPrefix,
    datagram.clientIdentifier,
    datagram.counterpartyClientIdentifier,
    datagram.version,
    datagram.delayPeriodTime,
    datagram.delayPeriodBlocks
  )
}

The ConnOpenTry datagram accepts a handshake request from an IBC module on another chain.

interface ConnOpenTry {
  counterpartyConnectionIdentifier: Identifier
  counterpartyPrefix: CommitmentPrefix
  counterpartyClientIdentifier: Identifier
  clientIdentifier: Identifier
  clientState: ClientState // DEPRECATED
  counterpartyVersions: string[]
  delayPeriodTime: uint64
  delayPeriodBlocks: uint64
  proofInit: CommitmentProof
  proofClient: CommitmentProof // DEPRECATED
  proofConsensus: CommitmentProof // DEPRECATED
  proofHeight: Height
  consensusHeight: Height // DEPRECATED
}
function handleConnOpenTry(datagram: ConnOpenTry) {
  handler.connOpenTry(
    datagram.counterpartyConnectionIdentifier,
    datagram.counterpartyPrefix,
    datagram.counterpartyClientIdentifier,
    datagram.clientIdentifier,
    datagram.clientState,
    datagram.counterpartyVersions,
    datagram.delayPeriodTime,
    datagram.delayPeriodBlocks,
    datagram.proofInit,
    datagram.proofClient,
    datagram.proofConsensus,
    datagram.proofHeight,
    datagram.consensusHeight
  )
}

The ConnOpenAck datagram confirms a handshake acceptance by the IBC module on another chain.

interface ConnOpenAck {
  identifier: Identifier
  clientState: ClientState // DEPRECATED
  version: string
  counterpartyIdentifier: Identifier
  proofTry: CommitmentProof
  proofClient: CommitmentProof // DEPRECATED
  proofConsensus: CommitmentProof // DEPRECATED
  proofHeight: Height
  consensusHeight: Height // DEPRECATED
}
function handleConnOpenAck(datagram: ConnOpenAck) {
  handler.connOpenAck(
    datagram.identifier,
    datagram.clientState,
    datagram.version,
    datagram.counterpartyIdentifier,
    datagram.proofTry,
    datagram.proofClient,
    datagram.proofConsensus,
    datagram.proofHeight,
    datagram.consensusHeight
  )
}

The ConnOpenConfirm datagram acknowledges a handshake acknowledgement by an IBC module on another chain & finalises the connection.

interface ConnOpenConfirm {
  identifier: Identifier
  proofAck: CommitmentProof
  proofHeight: Height
}
function handleConnOpenConfirm(datagram: ConnOpenConfirm) {
    handler.connOpenConfirm(
      datagram.identifier,
      datagram.proofAck,
      datagram.proofHeight
    )
}

Channel lifecycle management

interface ChanOpenInit {
  order: ChannelOrder
  connectionHops: [Identifier]
  portIdentifier: Identifier
  counterpartyPortIdentifier: Identifier
  version: string
}
function handleChanOpenInit(datagram: ChanOpenInit) {
  module = lookupModule(datagram.portIdentifier)
  channelIdentifier, channelCapability = handler.chanOpenInit(
    datagram.order,
    datagram.connectionHops,
    datagram.portIdentifier,
    datagram.counterpartyPortIdentifier
  )
  // SYNCHRONOUS: the following calls happen synchronously with the call above
  // ASYNCHRONOUS: the module callback will be called at a time later than the channel handler
  // in this case, the channel identifier will be stored with a sentinel value in the channel path so it is not taken
  // by a new channel handshake and the capability is reserved for the application module.
  // When the module eventually executes its callback it must call writeChannel so that the channel
  // can be written into an INIT state with the right version and the handshake can proceed on the counterparty.
  version, err = module.onChanOpenInit(
    datagram.order,
    datagram.connectionHops,
    datagram.portIdentifier,
    channelIdentifier,
    datagram.counterpartyPortIdentifier,
    datagram.version
  )
  abortTransactionUnless(err === nil)
  writeChannel(
    datagram.portIdentifier,
    channelIdentifier,
    INIT,
    datagram.order,
    datagram.counterpartyPortIdentifier,
    datagram.counterpartyChannelIdentifier,
    datagram.connectionHops,
    version
  )
}
interface ChanOpenTry {
  order: ChannelOrder
  connectionHops: [Identifier]
  portIdentifier: Identifier
  channelIdentifier: Identifier
  counterpartyPortIdentifier: Identifier
  counterpartyChannelIdentifier: Identifier
  counterpartyVersion: string
  proofInit: CommitmentProof
  proofHeight: Height
}
function handleChanOpenTry(datagram: ChanOpenTry) {
  module = lookupModule(datagram.portIdentifier)
  channelIdentifier, channelCapability = handler.chanOpenTry(
    datagram.order,
    datagram.connectionHops,
    datagram.portIdentifier,
    datagram.channelIdentifier,
    datagram.counterpartyPortIdentifier,
    datagram.counterpartyChannelIdentifier,
    datagram.counterpartyVersion,
    datagram.proofInit,
    datagram.proofHeight
  )
  // SYNCHRONOUS: the following calls happen synchronously with the call above
  // ASYNCHRONOUS: the module callback will be called at a time later than the channel handler
  // in this case, the channel identifier will be stored with a sentinel value in the channel path so it is not taken
  // by a new channel handshake and the capability is reserved for the application module.
  // When the module eventually executes its callback it must call writeChannel so that the channel
  // can be written into a TRY state with the right version and the handshake can proceed on the counterparty.
  version, err = module.onChanOpenTry(
    datagram.order,
    datagram.connectionHops,
    datagram.portIdentifier,
    channelIdentifier,
    datagram.counterpartyPortIdentifier,
    datagram.counterpartyChannelIdentifier,
    datagram.counterpartyVersion
  )
  abortTransactionUnless(err === nil)
  writeChannel(
    datagram.portIdentifier,
    channelIdentifier,
    TRYOPEN,
    datagram.order,
    datagram.counterpartyPortIdentifier,
    datagram.counterpartyChannelIdentifier,
    datagram.connectionHops,
    version
  )
}
interface ChanOpenAck {
  portIdentifier: Identifier
  channelIdentifier: Identifier
  counterpartyChannelIdentifier: Identifier
  counterpartyVersion: string
  proofTry: CommitmentProof
  proofHeight: Height
}
function handleChanOpenAck(datagram: ChanOpenAck) {
  module = lookupModule(datagram.portIdentifier)
  handler.chanOpenAck(
    datagram.portIdentifier,
    datagram.channelIdentifier,
    datagram.counterpartyChannelIdentifier,
    datagram.counterpartyVersion,
    datagram.proofTry,
    datagram.proofHeight
  )
  // SYNCHRONOUS: the following calls happen synchronously with the call above
  // ASYNCHRONOUS: the module callback will be called at a time later than the channel handler
  // When the module eventually executes its callback it must call writeChannel so that the channel
  // can be written into an OPEN state and the handshake can proceed on the counterparty.
  err = module.onChanOpenAck(
    datagram.portIdentifier,
    datagram.channelIdentifier,
    datagram.counterpartyChannelIdentifier,
    datagram.counterpartyVersion
  )
  abortTransactionUnless(err === nil)
  channel = provableStore.get(channelPath(datagram.portIdentifier, datagram.channelIdentifier))
  writeChannel(
    datagram.portIdentifier,
    datagram.channelIdentifier,
    OPEN,
    channel.order,
    channel.counterparty.portIdentifier,
    datagram.counterpartyChannelIdentifier,
    channel.connectionHops,
    datagram.counterpartyVersion
  )
}
interface ChanOpenConfirm {
  portIdentifier: Identifier
  channelIdentifier: Identifier
  proofAck: CommitmentProof
  proofHeight: Height
}
function handleChanOpenConfirm(datagram: ChanOpenConfirm) {
  module = lookupModule(datagram.portIdentifier)
  handler.chanOpenConfirm(
    datagram.portIdentifier,
    datagram.channelIdentifier,
    datagram.proofAck,
    datagram.proofHeight
  )
  // SYNCHRONOUS: the following calls happen synchronously with the call above
  // ASYNCHRONOUS: the module callback will be called at a time later than the channel handler
  // When the module eventually executes its callback it must call writeChannel so that the channel
  // can be written into an OPEN state and the handshake can proceed on the counterparty.
  err = module.onChanOpenConfirm(
    datagram.portIdentifier,
    datagram.channelIdentifier
  )
  abortTransactionUnless(err === nil)
  channel = provableStore.get(channelPath(datagram.portIdentifier, datagram.channelIdentifier))
  writeChannel(
    datagram.portIdentifier,
    datagram.channelIdentifier,
    OPEN,
    channel.order,
    channel.counterparty.portIdentifier,
    channel.counterparty.channelIdentifier,
    channel.connectionHops,
    channel.version
  )
}
interface ChanCloseInit {
  portIdentifier: Identifier
  channelIdentifier: Identifier
}
function handleChanCloseInit(datagram: ChanCloseInit) {
    module = lookupModule(datagram.portIdentifier)
    err = module.onChanCloseInit(
      datagram.portIdentifier,
      datagram.channelIdentifier
    )
    abortTransactionUnless(err === nil)
    handler.chanCloseInit(
      datagram.portIdentifier,
      datagram.channelIdentifier
    )
}
interface ChanCloseConfirm {
  portIdentifier: Identifier
  channelIdentifier: Identifier
  proofInit: CommitmentProof
  proofHeight: Height
}
function handleChanCloseConfirm(datagram: ChanCloseConfirm) {  
    handler.chanCloseConfirm(
      datagram.portIdentifier,
      datagram.channelIdentifier,
      datagram.proofInit,
      datagram.proofHeight
    )
    // SYNCHRONOUS: the following calls happen synchronously with the call above
    // ASYNCHRONOUS: the module callback will be called at a time later than the channel handler
    // When the module eventually executes its callback it must call writeChannel so that the channel
    // can be written into a CLOSED state and the handshake can proceed on the counterparty.
    module = lookupModule(datagram.portIdentifier)
    err = module.onChanCloseConfirm(
      datagram.portIdentifier,
      datagram.channelIdentifier
    )
    abortTransactionUnless(err === nil)
    writeChannel(
      datagram.portIdentifier,
      datagram.channelIdentifier,
      CLOSED,
    )
}

Packet relay

Packets are sent by the module directly (by the module calling the IBC handler).

interface PacketRecv {
  packet: Packet
  proof: CommitmentProof
  proofHeight: Height
}
function handlePacketRecv(datagram: PacketRecv) {
  module = lookupModule(datagram.packet.destPort)
  handler.recvPacket(
    datagram.packet,
    datagram.proof,
    datagram.proofHeight,
    acknowledgement
  )
  acknowledgement = module.onRecvPacket(datagram.packet)
  writeAcknowledgement(datagram.packet, acknowledgement)
}
interface PacketAcknowledgement {
  packet: Packet
  acknowledgement: string
  proof: CommitmentProof
  proofHeight: Height
}
function handlePacketAcknowledgement(datagram: PacketAcknowledgement) {
  module = lookupModule(datagram.packet.sourcePort)
  handler.acknowledgePacket(
    datagram.packet,
    datagram.acknowledgement,
    datagram.proof,
    datagram.proofHeight
  )
  module.onAcknowledgePacket(
    datagram.packet,
    datagram.acknowledgement
  )   
}

Packet timeouts

interface PacketTimeout {
  packet: Packet
  proof: CommitmentProof
  proofHeight: Height
  nextSequenceRecv: Maybe<uint64>
}
function handlePacketTimeout(datagram: PacketTimeout) {
  module = lookupModule(datagram.packet.sourcePort)
  handler.timeoutPacket(
    datagram.packet,
    datagram.proof,
    datagram.proofHeight,
    datagram.nextSequenceRecv
  )
  module.onTimeoutPacket(datagram.packet)
}
interface PacketTimeoutOnClose {
  packet: Packet
  proof: CommitmentProof
  proofHeight: Height
}
function handlePacketTimeoutOnClose(datagram: PacketTimeoutOnClose) {
  module = lookupModule(datagram.packet.sourcePort)
  handler.timeoutOnClose(
    datagram.packet,
    datagram.proof,
    datagram.proofHeight
  )
  module.onTimeoutPacket(datagram.packet)
}

Closure-by-timeout & packet cleanup

interface PacketCleanup {
  packet: Packet
  proof: CommitmentProof
  proofHeight: Height
  nextSequenceRecvOrAcknowledgement: Either<uint64, bytes>
}

Query (read-only) functions

All query functions for clients, connections, and channels should be exposed (read-only) directly by the IBC handler module.

Interface usage example

See ICS 20 for a usage example.

Properties & Invariants

  • Proxy port binding is first-come-first-serve: once a module binds to a port through the IBC routing module, only that module can utilise that port until the module releases it.

Backwards Compatibility

Not applicable.

Forwards Compatibility

routing modules are closely tied to the IBC handler interface.

Example Implementations

History

Jun 9, 2019 - Draft submitted

Jul 28, 2019 - Major revisions

Aug 25, 2019 - Major revisions

Mar 28, 2023 - Fix order of executing module handler and application callback

Copyright

All content herein is licensed under Apache 2.0.