概述

本文档标准规定了在任意 ICS 应用协议之上处理手续费支付所需的数据包数据结构、状态机处理逻辑以及编码细节。它需要对确认消息作出一些修改,但任何应用都可以采用,而无需强制其他应用使用该实现。

动机

关于面向中继者的通用激励机制,已经有过大量讨论。最初曾提出一个简单方案,试图扩展 ICS-20 以激励中继,在目标链上提供激励。然而,该方案过于针对 ICS-20,无法适用于其他协议。随后,这一思路被扩展为更通用的手续费支付设计,可供任意 ICS 应用协议采用。 总体而言,如果没有明确的方法来激励中继者,跨链互操作的愿景就无法扩展。我们的目标是定义一个清晰、易于被任何应用采用的接口,同时也不排斥那些不使用代币的链。

期望属性

  • 激励及时传递数据包(调用 recvPacket)
  • 激励为这些数据包中继确认消息(调用 acknowledgePacket)
  • 当超时时间已过且数据包尚未送达时,激励为这些数据包中继超时消息(例如接收费过低时)(调用 timeoutPacket)
  • 不产生额外的 IBC 数据包
  • 即使目标链不支持同质化代币概念,单向流程也能工作
  • 每条实现该机制的链都可自行选择是否启用。例如,链 A 上支持手续费的 ICS27 可以连接到链 B 上不支持手续费的 ICS27。
  • 为每条实现该扩展的链提供标准化接口
  • 在同一框架内支持自定义的手续费处理逻辑
  • 中继者地址不应可被伪造
  • 支持无许可或许可式中继

定义

forward relayer:为给定数据包提交 recvPacket 消息的中继者 reverse relayer:为给定数据包提交 acknowledgePacket 消息的中继者 timeout relayer:为给定数据包提交 timeoutPacket 或 timeoutOnClose 消息的中继者 receive fee:为给定数据包提交 recvPacket 消息所支付的手续费 ack fee:为给定数据包提交 acknowledgePacket 消息所支付的手续费 timeout fee:为给定数据包提交 timeoutPacket 或 timeoutOnClose 消息所支付的手续费 source address:中继者在发送该数据包的链上选择的收款地址 destination address:在接收该数据包的链上的中继者地址

技术规范

总体设计

为了避免产生数量级与应用数据包数相同的额外手续费数据包,并提供可选启用的方式,我们只在源链上存储所有手续费支付信息。源链是发送方能够提供代币来激励该数据包的唯一位置。手续费分配方式可以是具体实现相关的,因此无需写入 IBC 规范中(本文档只需要给出高层要求)。 我们要求向应用模块暴露中继者地址,以便所有与数据包相关的消息都能让模块激励数据包中继者。因此,acknowledgePacket、timeoutPacket 和 timeoutOnClose 消息将携带中继者地址,并能够将托管的代币发送到该地址。 不过,我们还需要一种可靠的方法,将在目标链上提交 recvPacket 的中继者地址传回源链。实际上,我们需要的是该中继者对应的 source address 用于支付,而不是对数据包签名的 destination address。 手续费支付机制将作为 IBC Middleware(见 ICS-30)实现,以便为应用开发者和区块链提供最大的灵活性。 基于此,流程如下:
  1. 中继者在目标链的手续费中间件中注册其 destination address 到 source address 的映射。
  2. 用户或模块在 source 链上提交发送数据包,同时向手续费中间件模块提交一条消息,附带一些代币以及如何分配这些代币的手续费信息。所有手续费代币都由手续费模块托管。
  3. RelayerA 在 destination 链上提交 RecvPacket。
  4. 目标链的手续费中间件会根据该中继者的 destination address 取回对应的 source address(该映射已预先注册),并将其写入确认消息中。
  5. RelayerB 提交 AcknowledgePacket,消息发送者中会提供 reverse relayer 在源链上的地址,同时确认消息中还嵌入了 forward relayer 的 source address。
  6. 源链的手续费中间件可以将步骤 (1) 中托管的代币分配给 forward 和 reverse 两个中继者,并将剩余代币退还给原始手续费支付方。
另一种流程:
  1. 用户或模块在 source 链上提交发送数据包,同时附带一些代币以及如何分配这些代币的手续费信息
  2. 中继者提交 OnTimeout,其中提供其在源链上的地址
  3. 源链应用可以将步骤 (1) 中托管的代币分配给该中继者,并可将剩余代币退还给原始手续费支付方

手续费细节

以 Cosmos SDK 中的一个实现为例,我们考虑 3 种可定义的潜在手续费支付。每一种都可以用不同的代币支付。设想 IrisNet 与 Cosmos Hub 之间建立了一条连接。为了激励从 IrisNet 发往 Cosmos Hub 的数据包,他们可以定义:
  • ReceiveFee: 0.003 channel-7/ATOM 凭证(已通过 ICS20 存在于 IrisNet 上的 ATOM)
  • AckFee: 0.001 IRIS
  • TimeoutFee: 0.002 IRIS
理想情况下,这些手续费在两侧都可以方便地以原生代币兑换,但中继者也可以选择其他代币。在这个例子中,中继者会获得相当数量的 IRIS,既覆盖了其在该链上的成本,也有所盈余。它还会从许多数据包中获得 channel-7/ATOM 凭证。在中继了几千个数据包之后,Cosmos Hub 上的账户余额开始不足,因此中继者会将这些 channel-7/ATOM 凭证通过 channel-7 发回其在 Hub 上的账户,以补充那里的余额。 发送链会从手续费支付方的账户中托管 0.003 channel-7/ATOM 和 0.002 IRIS。如果由前向中继者提交 recvPacket,并由反向中继者提交 ackPacket,那么前向中继者将获得 0.003 channel-7/ATOM,反向中继者将获得 0.001 IRIS,而 0.002 IRIS 会退还给原始手续费支付方。如果数据包发生超时,则超时中继者将获得 0.002 IRIS,而 0.003 channel-7/ATOM 会退还给原始手续费支付方。 向用户收取手续费并将其支付给相应中继者的逻辑,由独立的手续费模块封装,不同实现之间可以有所差异。然而,所有手续费模块都必须实现统一接口,以便 ICS-4 处理程序能够将手续费正确支付给对应的中继者,并使中继者自身能够方便地确定其在中继某个数据包时可预期获得的手续费。

数据结构

写入目标链的激励型确认消息包含:
  • 底层应用确认消息的原始字节,
  • 前向中继者的 source address,
  • 以及一个布尔值,用于指示底层应用的接收操作是否成功。
interface Acknowledgement {
    appAcknowledgement: []byte
    forwardRelayerAddress: string
    underlyingAppSuccess: boolean
}

存储路径

异步确认路径的中继者地址

前向中继者地址存储在一个以端口标识符、通道标识符和序列号组合唯一确定的存储路径前缀下。该信息可以存储在私有存储中。
function relayerAddressForAsyncAckPath(packet: Packet): Path {
    return "forwardRelayer/{packet.destPort}/{packet.destChannel}/{packet.sequence}"
}

费用中间件合约

尽管不同费用模块的实现细节可能有所不同,但所有费用模块必须确保做到以下几点:
  • 必须允许中继器注册其对手方收款地址(即源地址)。
  • 必须托管所有未完成数据包可能支付的最大费用(或者必须具备铸造所需代币数量的能力)。
  • 必须将某个数据包的接收费支付给 PayFee 回调中指定的正向中继器(如果未指定,则必须将正向费用退还给原始费用支付者)。
  • 必须将某个数据包的确认费支付给 PayFee 回调中指定的反向中继器。
  • 必须将某个数据包的超时费支付给 PayTimeoutFee 回调中指定的超时中继器。
  • 如适用,必须将托管中的剩余费用退还给原始费用支付者。
// RegisterCounterpartyPayee 由中继器在每个 channelEnd 上调用,
// 允许其在中继前指定自己的对手方收款地址。
// 这可以确保其因正向中继而获得正确补偿,因为
// 目标链必须在确认中回传中继器的源地址(对手方
// 收款地址)。
// 中继器可以多次调用此函数,在这种情况下,始终使用最新的
// 对手方收款地址。
function RegisterCounterpartyPayee(relayer: string, counterPartyAddress: string) {
    // 设置中继器地址与对手方收款地址之间的映射
}

// EscrowPacketFee 是一个开放回调,任何希望托管资金以激励
// 给定数据包中继的模块或用户都可以调用它。
// 注意:这些费用是在该数据包先前已托管金额的基础上额外托管的。
// 如果先前金额为零,则提供的费用即为初始托管金额。
// 他们可以为数据包流程中的每个步骤分别设置 receiveFee、ackFee 和 timeoutFee。
// 调用者必须向费用模块发送 max(receiveFee+ackFee, timeoutFee),
// 以锁定托管,从而为任何可能的数据包流程提供支付。
// 调用者还可以选择性地指定一个中继器地址数组。费用模块可以
// 根据最终中继器地址使用该数组来修改费用支付逻辑。
// 例如,如果中继器地址在 `EscrowPacketFee` 中被指定,
// 费用模块可以选择仅向该中继器支付费用。
function EscrowPacketFee(packet: Packet, receiveFee: Fee, ackFee: Fee, timeoutFee: Fee, relayers: []string) {
    // 为此数据包托管 max(receiveFee+ackFee, timeoutFee)
    // 如有必要,使用提供的中继器地址执行自定义逻辑
}

// PayFee 是由费用模块实现、并由 ICS-4 AcknowledgePacket 处理器调用的回调。
function PayFee(packet: Packet, forward_relayer: string, reverse_relayer: string) {
    // 将正向费用支付给正向中继器地址
    // 将反向费用支付给反向中继器地址
    // 将多余代币退还给原始费用支付者
    // 注意:如果正向中继器地址为空,则将正向费用退还给原始费用支付者。
}

// PayTimeoutFee 是由费用模块实现、并由 ICS-4 TimeoutPacket 处理器调用的回调。
function PayTimeoutFee(packet: Packet, timeout_relayer: string) {
    // 将超时费用支付给超时中继器地址
    // 将多余代币退还给原始费用支付者
}
费用模块还应暴露以下查询接口,以便中继器查询其预期可获得的费用:
// 获取为给定数据包提交 RecvPacket 消息时期望获得的费用
// 如果费用依赖于特定中继器,调用者应提供预期的中继器地址。
function GetReceiveFee(portID, channelID, sequence, relayer) Fee

// 获取为给定数据包提交 AcknowledgePacket 消息时期望获得的费用
// 如果费用依赖于特定中继器,调用者应提供预期的中继器地址。
function GetAckFee(portID, channelID, sequence, relayer) Fee

// 获取为给定数据包提交 TimeoutPacket 消息时期望获得的费用
// 如果费用依赖于特定中继器,调用者应提供预期的中继器地址。
function GetTimeoutFee(portID, channelID, sequence, relayer) Fee
由于不同链对同质化代币的表示方式可能不同,且这些信息不会发送到其他链;因此,本 ICS 不为 Fee 指定特定表示。每条链都可以选择自己的表示方式,中继器有责任正确解释 Fee。 默认表示将具有以下结构:
interface Fee {
  denom: string,
  amount: uint256,
}

IBC 模块包装器

费用中间件将实现其自己的 ICS-26 回调,这些回调会包装特定于应用的模块回调,以及由底层应用调用的 ICS-4 处理器函数。该费用中间件将确保对手方模块支持激励机制,并实现所有费用相关逻辑。随后,它会将请求传递给嵌入的应用模块,以进行进一步的回调处理。 通过这种方式,可以将自定义费用处理逻辑挂接到 IBC 数据包流转逻辑中,而无需将代码放入 ICS-4 处理器或应用代码中。这一点很有价值,因为 ICS-4 处理器应只关注 IBC 核心部分的正确性(传输、认证和排序),而应用处理器不应处理所有受激励应用通用的费用逻辑。事实上,一个给定的应用模块应当能够接入任何费用模块,而无需对应用本身做进一步修改。

费用协议协商

费用中间件将把自己的版本与应用版本一起包含在内,以此与对手方模块协商费用协议版本。通道版本将是一个 JSON 结构体的字符串,其中包含费用中间件版本和应用版本。如果应用栈由多个中间件包装一个基础应用组成,那么应用版本本身也可以是一个 JSON 编码字符串,并可能进一步包含更多中间件和应用版本。 通道版本:
{"fee_version":"<fee_protocol_version>","app_version":"<application_version>"}
示例:
{"fee_version":"ics29-1","app_version":"ics20-1"}
费用中间件的握手回调会确保两个模块就兼容的费用协议版本达成一致,然后将特定于应用的版本字符串传递给嵌入式应用的握手回调。

握手回调

function onChanOpenInit(
  order: ChannelOrder,
  connectionHops: [Identifier],
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyPortIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  version: string): (version: string, err: Error) {
    if version != "" {
        // 尝试反序列化 JSON 编码的版本字符串,并将
        // 应用特定版本传递给应用回调。
        // 否则,直接将 version 传递给应用回调。
        metadata, err = UnmarshalJSON(version)
        if err != nil {
            // 调用底层应用的 OnChanOpenInit 回调
            return app.onChanOpenInit(
                order,
                connectionHops,
                portIdentifier,
                channelIdentifier,
                counterpartyPortIdentifier,
                counterpartyChannelIdentifier,
                version,
            )
        }

        // 检查 feeVersion 是否受支持
        if !isSupported(metadata.feeVersion) {
            return "", error
        }
    } else {
        // 如果中继器未另行指定,则默认启用费用
        metadata = {
            feeVersion: "ics29-1",
            appVersion: "",
        }
    }

    // 调用底层应用的 OnChanOpenInit 回调。
    // 如果 version 字符串为空,则期望 OnChanOpenInit 返回
    // 一个表示其所支持版本的默认版本字符串
    appVersion, err = app.onChanOpenInit(
        order,
        connectionHops,
        portIdentifier,
        channelIdentifier,
        counterpartyPortIdentifier,
        counterpartyChannelIdentifier,
        metadata.appVersion,
    )
    if err != nil {
        return "", err
    }

    // 使用底层应用返回的应用版本构造一个新的版本字符串,
    // 以防它与调用者传入的版本不同
    version = constructVersion(metadata.feeVersion, appVersion)

    return version, nil
}

function onChanOpenTry(
  order: ChannelOrder,
  connectionHops: [Identifier],
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyPortIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  counterpartyVersion: string): (version: string, err: Error) {
    // 尝试反序列化 JSON 编码的版本字符串,并将
    // 应用特定版本传递给应用回调。
    // 否则,直接将 version 传递给应用回调。
    cpMetadata, err = UnmarshalJSON(counterpartyVersion)
    if err != nil {
        // 调用底层应用的 OnChanOpenTry 回调
        return app.onChanOpenTry(
            order,
            connectionHops,
            portIdentifier,
            channelIdentifier,
            counterpartyPortIdentifier,
            counterpartyChannelIdentifier,
            counterpartyVersion,
        )
    }

    // 选择双方兼容的费用版本
    if !isCompatible(cpMetadata.feeVersion) {
        return "", error
    }
    feeVersion = selectFeeVersion(cpMetadata.feeVersion)

    // 调用底层应用的 OnChanOpenTry 回调
    appVersion, err = app.onChanOpenTry(
        order,
        connectionHops,
        portIdentifier,
        channelIdentifier,
        counterpartyPortIdentifier,
        counterpartyChannelIdentifier,
        cpMetadata.appVersion,
    )
    if err != nil {
        return "", err
    }
    
    // 使用最终选定的费用版本以及底层应用返回的应用版本
    // 构造一个新的版本字符串
    // (它可能与调用者传入的版本不同)
    version = constructVersion(feeVersion, appVersion)

    return version, nil
}

function onChanOpenAck(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  counterpartyVersion: string) {
    cpMetadata, err = UnmarshalJSON(counterpartyVersion)
    if err != nil {
        // 调用底层应用的 OnChanOpenAck 回调
        return app.onChanOpenAck(
            portIdentifier, 
            channelIdentifier, 
            counterpartyChannelIdentifier,
            counterpartyVersion,
        )
    }

    if !isSupported(cpMetadata.feeVersion) {
        return error
    }  
    // 调用底层应用的 OnChanOpenAck 回调
    return app.onChanOpenAck(
        portIdentifier, 
        channelIdentifier, 
        counterpartyChannelIdentifier,
        cpMetadata.appVersion,
    )
}

function onChanOpenConfirm(
  portIdentifier: Identifier,
  channelIdentifier: Identifier) {
    // fee middleware 在 ChanOpenConfirm 上不执行任何操作,
    // 只调用底层回调
    return app.onChanOpenConfirm(portIdentifier, channelIdentifier)
}

数据包回调

function onRecvPacket(packet: Packet, relayer: string): bytes {
    app_acknowledgement = app.onRecvPacket(packet, relayer)

    // 如果是异步确认,我们必须存储中继器地址。
    // 之后会取回该地址,并用它获取将写入确认中的
    // 源地址。
    if app_acknowledgement == nil {
        privateStore.set(relayerAddressForAsyncAckPath(packet), relayer)
    }

    // 通过获取费用中间件中为该中继器存储的对手方收款地址
    // 来获取源地址。
    // 注意:源地址可能为空或无效,在这些情况下
    // 对手方必须退还费用
    sourceAddress = getCounterpartyPayeeAddress(relayer)

    // 用正向中继器包装确认并返回序列化后的字节
    // constructIncentivizedAck 接收以下参数:
    // - 应用特定的确认,
    // - 接收数据包的中继器(正向中继器)
    // - 以及一个表示接收操作是否成功的布尔值,
    // 并构造带激励的确认结构体,其中嵌入了
    // 正向中继器和应用特定确认。
    ack = constructIncentivizedAck(app_acknowledgment, sourceAddress, app_acknowledgment.success)
    return marshal(ack)
}

function onAcknowledgePacket(packet: Packet, acknowledgement: bytes, relayer: string) {
    // 该确认是一个序列化后的结构体,包含:
    // - 作为字符串的正向中继器地址(称为 forward_relayer)
    // - 以及对手方应用模块返回的原始确认字节(称为 app_ack)。

    // 从(带激励的)确认中获取正向中继器,
    // 并向正向与反向中继器支付费用。
    // reverse_relayer 是确认消息的提交者,
    // 由函数参数提供
    // 注意:费用可能为零
    ack = unmarshal(acknowledgement)
    forward_relayer = getForwardRelayer(ack)
    PayFee(packet, forward_relayer, relayer)

    // 解包对手方应用发送的原始确认字节,
    // 并将其传递给应用回调。
    app_ack = getAppAcknowledgement(acknowledgement)

    app.OnAcknowledgePacket(packet, app_ack, relayer)
}

function onTimeoutPacket(packet: Packet, relayer: string) {
    // 从函数参数中获取超时中继器
    // 并支付超时费用。
    // 注意:费用可能为零
    PayTimeoutFee(packet, relayer)
    app.OnTimeoutPacket(packet, relayer)
}

function onTimeoutPacketClose(packet: Packet, relayer: string) {
    // 从函数参数中获取超时中继器
    // 并支付超时费用。
    // 注意:费用可能为零
    PayTimeoutFee(packet, relayer)
    app.onTimeoutPacketClose(packet, relayer)
}

function constructIncentivizedAck(
  app_ack: bytes, 
  forward_relayer: string, 
  success: boolean): Acknowledgement {
    return Acknowledgement{
	appAcknowledgement:    app_ack,
	forwardRelayerAddress: relayer,
        underlyingAppSuccess:  success,
    }
}

function getForwardRelayer(ack: Acknowledgement): string {
    ack.forwardRelayerAddress
}

function getAppAcknowledgement(ack: Acknowledgement): bytes {
    ack.appAcknowledgement
}

调用 ICS-4 的嵌入式应用

请注意,如果嵌入式应用使用异步 ack,那么应用中的 WriteAcknowledgement 调用必须调用费用中间件的 WriteAcknowledgement,而不是直接调用 ICS-4 处理器的 WriteAcknowledgement 函数。
// 费用中间件的 writeAcknowledgement 函数
function writeAcknowledgement(
  packet: Packet,
  acknowledgement: bytes) {
    // 取回在 `onRecvPacket` 中存储的中继器
    relayer = privateStore.get(relayerAddressForAsyncAckPath(packet))
    // 通过获取费用中间件中为该中继器存储的对手方收款地址
    // 来获取源地址。
    sourceAddress = getCounterpartyPayeeAddress(relayer)
    ack = constructIncentivizedAck(acknowledgment, sourceAddress, acknowledgment.success)
    ack_bytes = marshal(ack)
    // ics4Wrapper 可以是核心 IBC 或更高层的中间件
    return ics4Wrapper.writeAcknowledgement(packet, ack_bytes)
}

// 费用中间件的 sendPacket 函数仅将数据转发给 ics-4 处理器
function sendPacket(
  capability: CapabilityKey,
  sourcePort: Identifier,
  sourceChannel: Identifier,
  timeoutHeight: Height,
  timeoutTimestamp: uint64,
  data: bytes): uint64 {
    // ics4Wrapper 可以是核心 IBC 或更高层的中间件
    return ics4Wrapper.sendPacket(
      capability,
      sourcePort,
      sourceChannel,
      timeoutHeight,
      timeoutTimestamp,
      data)
}

用户与费用中间件的交互

用户发送数据包 用户可以在提交数据包时指定一笔费用,以激励中继。具体做法是在应用特定的“发送数据包”消息(例如 ICS-20 MsgTransfer)之外,原子性地一并提交一条费用支付消息。费用中间件会为与该托管操作原子创建的数据包托管这笔费用。费用支付消息本身不在本文档中规定,因为它在不同实现之间可能差异很大。在某些中间件中,如果费用来自一个利他资金池,甚至可能根本不存在费用支付消息。 由于费用中间件不需要修改出站数据包,费用支付消息既可以放在发送数据包消息之前,也可以放在之后。不过,为了与其他中间件消息保持一致,建议费用中间件要求其消息放在发送数据包消息之前,并为给定通道上的下一个序列号托管费用。这样,当这些消息被原子提交时,通道上的下一个序列号对应的就是用户发送的数据包消息,用户也就为新创建的数据包托管了费用。 如果用户希望在数据包已经创建之后再支付费用,费用中间件 SHOULD 提供一条消息,允许用户按指定的序列号、通道标识符和端口标识符为某个数据包支付费用。这样用户就可以唯一标识一个已经创建的数据包,从而使费用中间件能够在事后为该数据包托管费用。 中继器发送 RecvPacket 在中继器开始在某个通道上进行中继之前,它应当使用标准化消息注册其对手方消息:
interface RegisterCounterpartyPayeeMsg {
    portID: string
    channelID: string
    relayer: string           // 正向中继器的目标地址
    counterpartyPayee: string // 正向中继器的源地址
}
接收链有责任验证该消息确实由 relayer 的所有者发送。接收链必须为给定通道存储如下映射:relayer -> counterpartyPayee。随后,目标费用中间件的 onRecvPacket 就可以查询 recvPacket 消息发送者的对手方收款地址,从而获取正向中继器的源地址。这个源地址会被嵌入到确认中。 如果中继器没有注册其对手方收款地址(或者注册了无效地址),确认仍然会被接收并处理,但正向费用将退还给原始费用支付者。

向后兼容性

如果要在费用模块内部直接维护与非激励链的向后兼容性,则需要顶层费用模块协商不包含费用版本的版本,并同时与激励模块和非激励模块通信。随着嵌套应用层级增加,这种模式会带来不必要的复杂性。 因此,费用模块只会连接到对手方费用模块。这样可以简化费用模块逻辑,也不需要它去模拟底层嵌套应用。 为了让激励链在某个特定应用(例如 ICS-20)上与非激励链保持向后兼容,激励链应同时托管一个顶层 ICS-20 模块和一个嵌套了 ICS-20 应用的顶层费用模块,并且两者都应绑定到各自唯一的端口。

论证

该提案满足所需属性。数据包流的所有部分(接收、确认、超时)都可以被正确激励和奖励。协议不会预先指定中继器,因此激励机制既可以是无许可的,也可以是许可制的。资金的托管和分发完全在源链上处理,因此费用协议不需要额外的 IBC 数据包,也不需要使用 ICS-20。费用协议只假设源链上存在同质化代币。通过为同一个基础应用创建应用栈(一个带费用中间件,一个不带),我们可以获得向后兼容性。
正确性
费用模块负责正确地托管资金,并将资金分发给指定的中继器。ack 和 timeout 中继器很容易获取,因为它们分别就是确认消息和超时消息的发送者。正向中继器负责在发送 recvPacket 消息之前注册其源地址,这样目标费用中间件就可以把该地址嵌入确认中。随后,源链上的费用中间件会使用确认中的地址,在源链上向正向中继器支付费用。 对于正向中继器地址,源链会采用“尽力而为”的处理方式。由于该地址不会被对手方直接验证,而只是被当作一个字符串回传到确认中,因此已注册的正向中继器源地址可能不是一个有效的源链地址。在这种情况下,无效地址会被丢弃,接收费会被退还,确认处理会继续进行。中继器有责任正确地向对手方链注册自己的源地址。 如果对手方链本身错误地发送了正向中继器地址,这会导致中继器无法因中继数据包而在源链上获得费用。受激励驱动的中继器会停止为该链中继,直到确认逻辑被修复,但通道本身仍然可用。 我们不能在源地址无效时返回错误,因为这会永久阻止源链处理一个本来已经在对手方链上被正确接收、处理并确认的数据包的确认。IBC 协议要求,不正确或恶意的中继器最多只能影响用户数据包的活性。如果在这种情况下阻止成功确认,数据包流将永久处于未完成状态,这对于某些 IBC 应用(例如 ICS-20)可能带来非常严重的后果。 因此,正向中继器是否能获得奖励,取决于它在发送 receive_packet 消息时是否提供了正确的 payOnSender 地址。即使费用支付失败,数据包流仍会继续成功处理。 当确认中正确嵌入了正向中继器,而反向与超时中继器又可直接从消息中获得时,费用中间件就能够准确地托管并分发费用给相关中继器。

可选附录

向前兼容性

不适用。

示例实现

历史

2021 年 6 月 8 日 - 从直接在 ICS-4 中实现回调切换为中间件方案。 2021 年 6 月 1 日 - 完成草案撰写 2022 年 7 月 6 日 - 根据实现中的最新变更更新

版权

本文所有内容均依据 Apache 2.0 许可。

Synopsis

This standard document specifies packet data structure, state machine handling logic, and encoding details for handling fee payments on top of any ICS application protocol. It requires some changes to the acknowledgement, but can be adopted by any application, without forcing other applications to use this implementation.

Motivation

There has been much discussion on a general incentivization mechanism for relayers. A simple proposal was created to extend ICS-20 to incentivize relaying on the destination chain. However, it was very specific to ICS-20 and would not work for other protocols. This was then extended to a more general fee payment design that could be adopted by any ICS application protocol. In general, the Interchain dream will never scale unless there is a clear way to incentivize relayers. We seek to define a clear interface that can be easily adopted by any application, but not preclude chains that don’t use tokens.

Desired Properties

  • Incentivize timely delivery of the packet (recvPacket called)
  • Incentivize relaying acks for these packets (acknowledgePacket called)
  • Incentivize relaying timeouts for these packets when the timeout has expired before packet is delivered (for example as receive fee was too low) (timeoutPacket called)
  • Produces no extra IBC packets
  • One direction works, even when destination chain does not support concept of fungible tokens
  • Opt-in for each chain implementing this. e.g. ICS27 with fee support on chain A could connect to ICS27 without fee support on chain B.
  • Standardized interface for each chain implementing this extension
  • Support custom fee-handling logic within the same framework
  • Relayer addresses should not be forgeable
  • Enable permissionless or permissioned relaying

Definitions

forward relayer: The relayer that submits the recvPacket message for a given packet reverse relayer: The relayer that submits the acknowledgePacket message for a given packet timeout relayer: The relayer that submits the timeoutPacket or timeoutOnClose message for a given packet receive fee: The fee paid for submitting the recvPacket message for a given packet ack fee: The fee paid for submitting the acknowledgePacket message for a given packet timeout fee: The fee paid for submitting the timeoutPacket or timeoutOnClose message for a given packet source address: The payee address selected by a relayer on the chain that sent the packet destination address: The address of a relayer on the chain that receives the packet

Technical Specification

General Design

In order to avoid extra fee packets on the order of the number of application packets, as well as provide an opt-in approach, we store all fee payment info only on the source chain. The source chain is the one location where the sender can provide tokens to incentivize the packet. The fee distribution may be implementation specific and thus does not need to be in the IBC spec (just high-level requirements are needed in this doc). We require that the relayer address is exposed to application modules for all packet-related messages, so the modules are able to incentivize the packet relayer. acknowledgePacket, timeoutPacket, and timeoutOnClose messages will therefore have the relayer address and be capable of sending escrowed tokens to such address. However, we need a way to reliably get the address of the relayer that submitted recvPacket on the destination chain to the source chain. In fact, we need a source address for this relayer to pay out to, not the destination address that signed the packet. The fee payment mechanism will be implemented as IBC Middleware (see ICS-30) in order to provide maximum flexibility for application developers and blockchains. Given this, the flow would be:
  1. Relayer registers their destination address to source address mapping on the destination chain’s fee middleware.
  2. User/module submits a send packet on the source chain, along with a message to the fee middleware module with some tokens and fee information on how to distribute them. The fee tokens are all escrowed by the fee module.
  3. RelayerA submits RecvPacket on the destination chain.
  4. Destination fee middleware will retrieve the source address for the given relayer’s destination address (this mapping is already registered) and include it in the acknowledgement.
  5. RelayerB submits AcknowledgePacket which provides the reverse relayer address on the source chain in the message sender, along with the source address of the forward relayer embedded in the acknowledgement.
  6. Source fee middleware can distribute the tokens escrowed in (1) to both the forward and the reverse relayers and refund remainder tokens to original fee payer(s).
Alternate flow:
  1. User/module submits a send packet on the source chain, along with some tokens and fee information on how to distribute them
  2. Relayer submits OnTimeout which provides its address on the source chain
  3. Source application can distribute the tokens escrowed in (1) to this relayer, and potentially return remainder tokens to the original fee payer(s).

Fee details

For an example implementation in the Cosmos SDK, we consider 3 potential fee payments, which may be defined. Each one may be paid out in a different token. Imagine a connection between IrisNet and the Cosmos Hub. To incentivize a packet from IrisNet to the Cosmos Hub, they may define:
  • ReceiveFee: 0.003 channel-7/ATOM vouchers (ATOMs already on IrisNet via ICS20)
  • AckFee: 0.001 IRIS
  • TimeoutFee: 0.002 IRIS
Ideally the fees can easily be redeemed in native tokens on both sides, but relayers may select others. In this example, the relayer collects a fair bit of IRIS, covering its costs there and more. It also collects channel-7/ATOM vouchers from many packets. After relaying a few thousand packets, the account on the Cosmos Hub is running low, so the relayer will send those channel-7/ATOM vouchers back over channel-7 to it’s account on the Hub to replenish the supply there. The sender chain will escrow 0.003 channel-7/ATOM and 0.002 IRIS from the fee payers’ account. In the case that a forward relayer submits the recvPacket and a reverse relayer submits the ackPacket, the forward relayer is rewarded 0.003 channel-7/ATOM and the reverse relayer is rewarded 0.001 IRIS while 0.002 IRIS is refunded to the original fee payer. In the case where the packet times out, the timeout relayer receives 0.002 IRIS and 0.003 channel-7/ATOM is refunded to the original fee payer. The logic involved in collecting fees from users and then paying it out to the relevant relayers is encapsulated by a separate fee module and may vary between implementations. However, all fee modules must implement a uniform interface such that the ICS-4 handlers can correctly pay out fees to the right relayers, and so that relayers themselves can easily determine the fees they can expect for relaying a packet.

Data Structures

The incentivized acknowledgment written on the destination chain includes:
  • raw bytes of the acknowledgement from the underlying application,
  • the source address of the forward relayer,
  • and a boolean indicative of receive operation success on the underlying application.
interface Acknowledgement {
    appAcknowledgement: []byte
    forwardRelayerAddress: string
    underlyingAppSuccess: boolean
}

Store Paths

Relayer Address for Async Ack Path

The forward relayer addresses are stored under a store path prefix unique to a combination of port identifier, channel identifier and sequence. This may be stored in the private store.
function relayerAddressForAsyncAckPath(packet: Packet): Path {
    return "forwardRelayer/{packet.destPort}/{packet.destChannel}/{packet.sequence}"
}

Fee Middleware Contract

While the details may vary between fee modules, all fee modules must ensure they does the following:
  • It must allow relayers to register their counterparty payee address (i.e. source address).
  • It must have in escrow the maximum fees that all outstanding packets may pay out (or it must have ability to mint required amount of tokens)
  • It must pay the receive fee for a packet to the forward relayer specified in PayFee callback (if unspecified, it must refund forward fee to original fee payer(s))
  • It must pay the ack fee for a packet to the reverse relayer specified in PayFee callback
  • It must pay the timeout fee for a packet to the timeout relayer specified in PayTimeoutFee callback
  • It must refund any remainder fees in escrow to the original fee payer(s) if applicable
// RegisterCounterpartyPayee is called by the relayer on each channelEnd and 
// allows them to specify their counterparty payee address before relaying.
// This ensures they will be properly compensated for forward relaying since 
// destination chain must send back relayer's source address (counterparty 
// payee address) in acknowledgement.
// This function may be called more than once by relayer, in which case, latest 
// counterparty payee address is always used.
function RegisterCounterpartyPayee(relayer: string, counterPartyAddress: string) {
    // set mapping between relayer address and counterparty payee address
}

// EscrowPacketFee is an open callback that may be called by any module/user 
// that wishes to escrow funds in order to incentivize the relaying of the 
// given packet.
// NOTE: These fees are escrowed in addition to any previously escrowed amount 
// for the packet. In the case where the previous amount is zero, the provided 
// fees are the initial escrow amount.
// They may set a separate receiveFee, ackFee, and timeoutFee to be paid
// for each step in the packet flow. The caller must send max(receiveFee+ackFee, timeoutFee)
// to the fee module to be locked in escrow to provide payout for any potential 
// packet flow.
// The caller may optionally specify an array of relayer addresses. This MAY be
// used by the fee module to modify fee payment logic based on ultimate relayer
// address. For example, fee module may choose to only pay out relayer if the 
// relayer address was specified in the `EscrowPacketFee`.
function EscrowPacketFee(packet: Packet, receiveFee: Fee, ackFee: Fee, timeoutFee: Fee, relayers: []string) {
    // escrow max(receiveFee+ackFee, timeoutFee) for this packet
    // do custom logic with provided relayer addresses if necessary
}

// PayFee is a callback implemented by fee module called by the ICS-4 AcknowledgePacket handler.
function PayFee(packet: Packet, forward_relayer: string, reverse_relayer: string) {
    // pay the forward fee to the forward relayer address
    // pay the reverse fee to the reverse relayer address
    // refund extra tokens to original fee payer(s)
    // NOTE: if forward relayer address is empty, then refund the forward fee to original fee payer(s).
}

// PayTimeoutFee is a callback implemented by fee module called by the ICS-4 TimeoutPacket handler.
function PayTimeoutFee(packet: Packet, timeout_relayer: string) {
    // pay the timeout fee to the timeout relayer address
    // refund extra tokens to original fee payer(s)
}
The fee module should also expose the following queries so that relayers may query their expected fee:
// Gets the fee expected for submitting RecvPacket msg for the given packet
// Caller should provide the intended relayer address in case the fee is dependent on specific relayer(s).
function GetReceiveFee(portID, channelID, sequence, relayer) Fee

// Gets the fee expected for submitting AcknowledgePacket msg for the given packet
// Caller should provide the intended relayer address in case the fee is dependent on specific relayer(s).
function GetAckFee(portID, channelID, sequence, relayer) Fee

// Gets the fee expected for submitting TimeoutPacket msg for the given packet
// Caller should provide the intended relayer address in case the fee is dependent on specific relayer(s).
function GetTimeoutFee(portID, channelID, sequence, relayer) Fee
Since different chains may have different representations for fungible tokens and this information is not being sent to other chains; this ICS does not specify a particular representation for the Fee. Each chain may choose its own representation, it is incumbent on relayers to interpret the Fee correctly. A default representation will have the following structure:
interface Fee {
  denom: string,
  amount: uint256,
}

IBC Module Wrapper

The fee middleware will implement its own ICS-26 callbacks that wrap the application-specific module callbacks as well as the ICS-4 handler functions called by the underlying application. This fee middleware will ensure that the counterparty module supports incentivization and will implement all fee-specific logic. It will then pass on the request to the embedded application module for further callback processing. In this way, custom fee-handling logic can be hooked up to the IBC packet flow logic without placing the code in the ICS-4 handlers or the application code. This is valuable since the ICS-4 handlers should only be concerned with correctness of core IBC (transport, authentication, and ordering), and the application handlers should not be handling fee logic that is universal amongst all other incentivized applications. In fact, a given application module should be able to be hooked up to any fee module with no further changes to the application itself.

Fee Protocol Negotiation

The fee middleware will negotiate its fee protocol version with the counterparty module by including its own version next to the application version. The channel version will be a string of a JSON struct containing the fee middleware version and the application version. The application version may as well be a JSON-encoded string, possibly including further middleware and app versions, if the application stack consists of multiple milddlewares wrapping a base application. Channel Version:
{"fee_version":"<fee_protocol_version>","app_version":"<application_version>"}
Ex:
{"fee_version":"ics29-1","app_version":"ics20-1"}
The fee middleware’s handshake callbacks ensure that both modules agree on compatible fee protocol version(s), and then pass the application-specific version string to the embedded application’s handshake callbacks.

Handshake Callbacks

function onChanOpenInit(
  order: ChannelOrder,
  connectionHops: [Identifier],
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyPortIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  version: string): (version: string, err: Error) {
    if version != "" {
        // try to unmarshal JSON-encoded version string and pass 
        // the app-specific version to app callback.
        // otherwise, pass version directly to app callback.
        metadata, err = UnmarshalJSON(version)
        if err != nil {
            // call the underlying applications OnChanOpenInit callback
            return app.onChanOpenInit(
                order,
                connectionHops,
                portIdentifier,
                channelIdentifier,
                counterpartyPortIdentifier,
                counterpartyChannelIdentifier,
                version,
            )
        }

        // check that feeVersion is supported
        if !isSupported(metadata.feeVersion) {
            return "", error
        }
    } else {
        // enable fees by default if relayer does not specify otherwise
        metadata = {
            feeVersion: "ics29-1",
            appVersion: "",
        }
    }

    // call the underlying application's OnChanOpenInit callback.
    // if the version string is empty, OnChanOpenInit is expected to return
    // a default version string representing the version(s) it supports
    appVersion, err = app.onChanOpenInit(
        order,
        connectionHops,
        portIdentifier,
        channelIdentifier,
        counterpartyPortIdentifier,
        counterpartyChannelIdentifier,
        metadata.appVersion,
    )
    if err != nil {
        return "", err
    }

    // a new version string is constructed with the app version returned 
    // by the underlying application, in case it is different than the 
    // one passed by the caller
    version = constructVersion(metadata.feeVersion, appVersion)

    return version, nil
}

function onChanOpenTry(
  order: ChannelOrder,
  connectionHops: [Identifier],
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyPortIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  counterpartyVersion: string): (version: string, err: Error) {
    // try to unmarshal JSON-encoded version string and pass 
    // the app-specific version to app callback.
    // otherwise, pass version directly to app callback.
    cpMetadata, err = UnmarshalJSON(counterpartyVersion)
    if err != nil {
        // call the underlying application's OnChanOpenTry callback
        return app.onChanOpenTry(
            order,
            connectionHops,
            portIdentifier,
            channelIdentifier,
            counterpartyPortIdentifier,
            counterpartyChannelIdentifier,
            counterpartyVersion,
        )
    }

    // select mutually compatible fee version
    if !isCompatible(cpMetadata.feeVersion) {
        return "", error
    }
    feeVersion = selectFeeVersion(cpMetadata.feeVersion)

    // call the underlying application's OnChanOpenTry callback
    appVersion, err = app.onChanOpenTry(
        order,
        connectionHops,
        portIdentifier,
        channelIdentifier,
        counterpartyPortIdentifier,
        counterpartyChannelIdentifier,
        cpMetadata.appVersion,
    )
    if err != nil {
        return "", err
    }
    
    // a new version string is constructed with the final fee version
    // that is selected and the app version returned by the underlying
    // application (which may be different than the one passed by the caller)
    version = constructVersion(feeVersion, appVersion)

    return version, nil
}

function onChanOpenAck(
  portIdentifier: Identifier,
  channelIdentifier: Identifier,
  counterpartyChannelIdentifier: Identifier,
  counterpartyVersion: string) {
    cpMetadata, err = UnmarshalJSON(counterpartyVersion)
    if err != nil {
        // call the underlying application's OnChanOpenAck callback
        return app.onChanOpenAck(
            portIdentifier, 
            channelIdentifier, 
            counterpartyChannelIdentifier,
            counterpartyVersion,
        )
    }

    if !isSupported(cpMetadata.feeVersion) {
        return error
    }  
    // call the underlying application's OnChanOpenAck callback
    return app.onChanOpenAck(
        portIdentifier, 
        channelIdentifier, 
        counterpartyChannelIdentifier,
        cpMetadata.appVersion,
    )
}

function onChanOpenConfirm(
  portIdentifier: Identifier,
  channelIdentifier: Identifier) {
    // fee middleware performs no-op on ChanOpenConfirm,
    // just call underlying callback
    return app.onChanOpenConfirm(portIdentifier, channelIdentifier)
}

Packet Callbacks

function onRecvPacket(packet: Packet, relayer: string): bytes {
    app_acknowledgement = app.onRecvPacket(packet, relayer)

    // in case of asynchronous acknowledgement, we must store the relayer
    // address. It will be retrieved later and used to get the source 
    // address that will be written in the acknowledgement.
    if app_acknowledgement == nil {
        privateStore.set(relayerAddressForAsyncAckPath(packet), relayer)
    }

    // get source address by retrieving counterparty payee address of 
    // this relayer stored in fee middleware.
    // NOTE: source address may be empty or invalid, counterparty
    // must refund fee in these cases
    sourceAddress = getCounterpartyPayeeAddress(relayer)

    // wrap the acknowledgement with forward relayer and return marshalled bytes
    // constructIncentivizedAck takes:
    // - the app-specific acknowledgement,
    // - the receive-packet relayer (forward relayer)
    // - and a boolean indicative of receive operation success,
    // and constructs the incentivized acknowledgement struct with 
    // the forward relayer and app-specific acknowledgement embedded.
    ack = constructIncentivizedAck(app_acknowledgment, sourceAddress, app_acknowledgment.success)
    return marshal(ack)
}

function onAcknowledgePacket(packet: Packet, acknowledgement: bytes, relayer: string) {
    // the acknowledgement is a marshalled struct containing:
    // - the forward relayer address as a string (called forward_relayer)
    // - and the raw acknowledgement bytes returned by the counterparty application module (called app_ack).

    // get the forward relayer from the (incentivized) acknowledgement
    // and pay fees to forward and reverse relayers.
    // reverse_relayer is submitter of acknowledgement message
    // provided in function arguments
    // NOTE: Fee may be zero
    ack = unmarshal(acknowledgement)
    forward_relayer = getForwardRelayer(ack)
    PayFee(packet, forward_relayer, relayer)

    // unwrap the raw acknowledgement bytes sent by counterparty application
    // and pass it to the application callback.
    app_ack = getAppAcknowledgement(acknowledgement)

    app.OnAcknowledgePacket(packet, app_ack, relayer)
}

function onTimeoutPacket(packet: Packet, relayer: string) {
    // get the timeout relayer from function arguments
    // and pay timeout fee.
    // NOTE: Fee may be zero
    PayTimeoutFee(packet, relayer)
    app.OnTimeoutPacket(packet, relayer)
}

function onTimeoutPacketClose(packet: Packet, relayer: string) {
    // get the timeout relayer from function arguments
    // and pay timeout fee.
    // NOTE: Fee may be zero
    PayTimeoutFee(packet, relayer)
    app.onTimeoutPacketClose(packet, relayer)
}

function constructIncentivizedAck(
  app_ack: bytes, 
  forward_relayer: string, 
  success: boolean): Acknowledgement {
    return Acknowledgement{
	appAcknowledgement:    app_ack,
	forwardRelayerAddress: relayer,
        underlyingAppSuccess:  success,
    }
}

function getForwardRelayer(ack: Acknowledgement): string {
    ack.forwardRelayerAddress
}

function getAppAcknowledgement(ack: Acknowledgement): bytes {
    ack.appAcknowledgement
}

Embedded applications calling into ICS-4

Note that if the embedded application uses asynchronous acks then, the WriteAcknowledgement call in the application must call the fee middleware’s WriteAcknowledgement rather than calling the ICS-4 handler’s WriteAcknowledgement function directly.
// Fee Middleware writeAcknowledgement function
function writeAcknowledgement(
  packet: Packet,
  acknowledgement: bytes) {
    // retrieve the relayer that was stored in `onRecvPacket`
    relayer = privateStore.get(relayerAddressForAsyncAckPath(packet))
    // get source address by retrieving counterparty payee address 
    // of this relayer stored in fee middleware.
    sourceAddress = getCounterpartyPayeeAddress(relayer)
    ack = constructIncentivizedAck(acknowledgment, sourceAddress, acknowledgment.success)
    ack_bytes = marshal(ack)
    // ics4Wrapper may be core IBC or higher-level middleware
    return ics4Wrapper.writeAcknowledgement(packet, ack_bytes)
}

// Fee Middleware sendPacket function just forwards data to ics-4 handler
function sendPacket(
  capability: CapabilityKey,
  sourcePort: Identifier,
  sourceChannel: Identifier,
  timeoutHeight: Height,
  timeoutTimestamp: uint64,
  data: bytes): uint64 {
    // ics4Wrapper may be core IBC or higher-level middleware
    return ics4Wrapper.sendPacket(
      capability,
      sourcePort,
      sourceChannel,
      timeoutHeight,
      timeoutTimestamp,
      data)
}

User Interaction with Fee Middleware

User sending Packets A user may specify a fee to incentivize the relaying during packet submission, by submitting a fee payment message atomically with the application-specific “send packet” message (e.g. ICS-20 MsgTransfer). The fee middleware will escrow the fee for the packet that is created atomically with the escrow. The fee payment message itself is not specified in this document as it may vary greatly across implementations. In some middleware, there may be no fee payment message at all if the fees are being paid out from an altruistic pool. Since the fee middleware does not need to modify the outgoing packet, the fee payment message may be placed before or after the send packet message. However in order to maintain consistency with other middleware messages, it is recommended that fee middleware require their messages to be placed before the send packet message and escrow fees for the next sequence on the given channel. This way when the messages are atomically committed, the next sequence on the channel is the send packet message sent by the user, and the user escrows their fee for the created packet. In case a user wants to pay fees on a packet after it has already been created, the fee middleware SHOULD provide a message that allows users to pay fees on a packet with the specified sequence, channel and port identifiers. This allows the user to uniquely identify a packet that has already been created, so that the fee middleware can escrow fees for that packet after the fact. Relayers sending RecvPacket Before a relayer starts relaying on a channel, they should register their counterparty message using the standardized message:
interface RegisterCounterpartyPayeeMsg {
    portID: string
    channelID: string
    relayer: string           // destination address of the forward relayer
    counterpartyPayee: string // source address of the forward relayer
}
It is the responsibility of the receiving chain to authenticate that the message was received from owner of relayer. The receiving chain must store the mapping from: relayer -> counterpartyPayee for the given channel. Then, onRecvPacket of the destination fee middleware can query for the counterparty payee address of the recvPacket message sender in order to get the source address of the forward relayer. This source address is what will get embedded in the acknowledgement. If the relayer does not register their counterparty payee address (or registers an invalid address), then the acknowledgment will still be received and processed but the forward fee will be refunded to the original fee payer(s).

Backwards Compatibility

Maintaining backwards compatibility with an unincentivized chain directly in the fee module, would require the top-level fee module to negotiate versions that do not contain a fee version and communicate with both incentivized and unincentivized modules. This pattern causes unnecessary complexity as the layers of nested applications increase. Instead, the fee module will only connect to a counterparty fee module. This simplifies the fee module logic, and doesn’t require it to mimic the underlying nested application(s). In order for an incentivized chain to maintain backwards compatibility with an unincentivized chain for a given application (e.g. ICS-20), the incentivized chain should host both a top-level ICS-20 module and a top-level fee module that nests an ICS-20 application each of which should bind to unique ports.

Reasoning

This proposal satisfies the desired properties. All parts of the packet flow (receive/acknowledge/timeout) can be properly incentivized and rewarded. The protocol does not specify the relayer beforehand, thus the incentivization can be permissionless or permissioned. The escrowing and distribution of funds is completely handled on source chain, thus there is no need for additional IBC packets or the use of ICS-20 in the fee protocol. The fee protocol only assumes existence of fungible tokens on the source chain. By creating application stacks for the same base application (one with fee middleware, one without), we can get backwards compatibility.
Correctness
The fee module is responsible for correctly escrowing and distributing funds to the provided relayers. The ack and timeout relayers are trivially retrievable since they are the senders of the acknowledgment and timeout message. The forward relayer is responsible for registering their source address before sending recvPacket messages, so that the destination fee middleware can embed this address in the acknowledgement. The fee middleware on source will then use the address in acknowledgement to pay the forward relayer on the source chain. The source chain will use a “best efforts” approach with regard to the forward relayer address. Since it is not verified directly by the counterparty and is instead just treated as a string to be passed back in the acknowledgement, the registered forward relayer source address may not be a valid source chain address. In this case, the invalid address is discarded, the receive fee is refunded, and the acknowledgement processing continues. It is incumbent on relayers to register their source addresses to the counterparty chain correctly. In the event that the counterparty chain itself incorrectly sends the forward relayer address, this will cause relayers to not collect fees on source chain for relaying packets. The incentivize-driven relayers will stop relaying for the chain until the acknowledgement logic is fixed, however the channel remains functional. We cannot return an error on an invalid source address as this would permanently prevent the source chain from processing the acknowledgment of a packet that was otherwise correctly received, processed and acknowledged on the counterparty chain. The IBC protocol requires that incorrect or malicious relayers may at best affect the liveness of a user’s packets. Preventing successful acknowledgement in this case would leave the packet flow at a permanently incomplete state, which may be very consequential for certain IBC applications like ICS-20. Thus, the forward relayer reward is contingent on it providing the correct payOnSender address when it sends the receive_packet message. The packet flow will continue processing successfully even if the fee payment is unsuccessful. With the forward relayer correctly embedded in the acknowledgement, and the reverse and timeout relayers available directly in the message; the fee middleware will accurately escrow and distribute fee payments to the relevant relayers.

Optional addenda

Forwards Compatibility

Not applicable.

Example Implementations

History

June 8 2021 - Switched to middleware solution from implementing callbacks in ICS-4 directly. June 1 2021 - Draft written July 6, 2022 - Update with latest changes from implementation All content herein is licensed under Apache 2.0.