ics | title | stage | category | kind | version compatibility | author | created | modified |
---|---|---|---|---|---|---|---|---|
26 |
Routing Module |
Draft |
IBC/TAO |
instantiation |
ibc-go v7.0.0 |
Christopher Goes <[email protected]> |
2019-06-09 |
2019-08-25 |
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.
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.
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
- 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.
Note: If the host state machine is utilising object capability authentication (see ICS 005), all functions utilising ports take an additional capability parameter.
Modules must expose the following function signatures to the routing module, which are called upon the receipt of various datagrams:
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
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
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
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}"
}
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))
}
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.
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)
}
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
counterpartyVersions: string[]
delayPeriodTime: uint64
delayPeriodBlocks: uint64
proofInit: CommitmentProof
proofClient: CommitmentProof
proofConsensus: CommitmentProof
proofHeight: Height
consensusHeight: Height
}
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
version: string
counterpartyIdentifier: Identifier
proofTry: CommitmentProof
proofClient: CommitmentProof
proofConsensus: CommitmentProof
proofHeight: Height
consensusHeight: Height
}
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
)
}
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,
)
}
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
)
}
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)
}
interface PacketCleanup {
packet: Packet
proof: CommitmentProof
proofHeight: Height
nextSequenceRecvOrAcknowledgement: Either<uint64, bytes>
}
All query functions for clients, connections, and channels should be exposed (read-only) directly by the IBC handler module.
See ICS 20 for a usage example.
- 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.
Not applicable.
routing modules are closely tied to the IBC handler interface.
- Implementation of ICS 26 in Go can be found in ibc-go repository.
- Implementation of ICS 26 in Rust can be found in ibc-rs repository.
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
All content herein is licensed under Apache 2.0.