Packet V2 结构

IBC 数据包将应用数据从源链发送到目标链,并带有一个超时参数,用于指定该数据包何时失效。源链会按照 ICS-24 规范中的定义对该数据包进行提交。随后,接收链会在 ICS-24 指定的数据包承诺路径下验证该数据包承诺。如果证明成功,IBC 处理器会将应用数据发送给相应的应用。
interface Packet {
    // identifier for the destination-chain client existing on source chain
    sourceClientId: bytes,
    // identifier for the source-chain client existing on destination chain
    destClientId: bytes,
    // the sequence uniquely identifies this packet
    // in the stream of packets from source to dest chain
    sequence: uint64,
    // the timeout is the timestamp in seconds on the destination chain
    // at which point the packet is no longer valid.
    // It cannot be received on the destination chain and can
    // be timed out on the source chain
    timeout: uint64,
    // the data includes the messages that are intended
    // to be sent to application(s) on the destination chain
    // from application(s) on the source chain
    // IBC core handlers will route the payload to the desired
    // application using the port identifiers but the rest of the
    // payload will be processed by the application
    data: [Payload]
}

interface Payload {
    // sourcePort identifies the sending application on the source chain
    sourcePort: bytes,
    // destPort identifies the receiving application on the dest chain
    destPort: bytes,
    // version identifies the version that sending application
    // expects destination chain to use in processing the message
    // if dest chain does not support the version, the payload must
    // be rejected with an error acknowledgement
    version: string,
    // encoding allows the sending application to specify which
    // encoding was used to encode the app data
    // the receiving applicaton will decode the appData into
    // the strucure expected given the version provided
    // if the encoding is not supported, receiving application
    // must be rejected with an error acknowledgement.
    // the encoding string MUST be in MIME format
    encoding: string,
    // appData is the opaque content sent from the source application
    // to the dest application. It will be decoded and interpreted
    // as specified by the version and encoding fields
    appData: bytes,
}
数据包顶层的源客户端标识符和目标客户端标识符用于标识正在通信的链。sourceClientId 标识符在源链上必须是唯一的,并指向源链上的目标链客户端。destClientId 标识符在目标链上必须是唯一标识符,并指向目标链上的源链客户端。sequence 是一个单调递增的 nonce,用于唯一标识在源链与目标链之间发送的数据包。 timeout 是一个 UNIX 时间戳,单位为秒;当目标链上的该时间点过去之后,数据包即失效,不能再被接收。请注意,超时时间戳是根据目标链的时钟来判断的,而该时钟相对于发送链或第三方观察者的时钟可能存在漂移。如果某个数据包在相对于目标链时钟已超过超时时间戳之后才在目标链上被接收;则该数据包必须被拒绝,以便发送链能够安全地将其超时并回滚。 在 IBC 规范的第 2 版中,具体实现可以支持在同一个数据包中包含多个应用数据。这可以表示为一个 payload 列表。实现也可以选择每个数据包只支持单个 payload;在这种情况下,它们只需拒绝那些带有多个 payload 的入站数据包。 每个 payload 都会包含各自的 Encoding 和 AppVersion,并传递给应用,以指示其如何解码和解释不透明的应用数据。应用必须支持所提供的 Encoding 和 AppVersion,才能处理 AppData。如果接收应用不支持该编码或应用版本,那么该应用必须向 IBC core 返回错误。如果接收应用支持所提供的编码和应用版本,那么它必须按照 Encoding 字符串指定的方式对应用数据进行解码,然后按照对端基于约定应用版本所期望的方式处理该应用。由于 Encoding 和 AppVersion 现在位于每个数据包中,因此它们可以按数据包粒度进行变更,一个应用也可以同时支持来自对端的多种编码和应用版本。这与 IBC 第 1 版形成鲜明对比:在第 1 版中,通道会预先协商通道版本(也隐式协商了编码);因此在通道打开之后再变更应用版本会非常困难。 所有实现都必须以标准化的 IBC 承诺格式对数据包进行提交,以满足协议要求。为此,我们首先必须提交数据包数据和超时值。超时值使用小端格式编码。数据包数据是一个 payload 列表,其提交方式是对 payload 的每个字段分别哈希,然后按顺序拼接在一起再进行处理。这可确保对给定数据包生成一个标准且无歧义的承诺。因此,任意一个给定数据包在所有兼容实现中都会生成完全相同的承诺,而两个不同的数据包在兼容实现中绝不会生成相同的承诺。然后,该承诺值会存储在下面定义的标准化、可证明的数据包承诺键下:
func packetCommitmentPath(packet: Packet): bytes {
    return packet.sourceClientId + byte(0x01) + bigEndian(packet.sequence)
}
// commitPayload hashes all the fields of the packet data to create a standard size
// preimage before committing it in the packet.
func commitPayload(payload: Payload): bytes {
    buffer = sha256.Hash(payload.sourcePort)
    buffer = append(sha256.Hash(payload.destPort))
    buffer = append(sha256.Hash(payload.version))
    buffer = append(sha256.Hash(payload.encoding))
    buffer = append(sha256.Hash(payload.appData))
    return sha256.Hash(buffer)
}

// commitV2Packet commits to all fields in the packet
// by hashing each individual field and then hashing these fields together
// Note: SourceClient and the sequence are omitted since they will be included in the key
// Every other field of the packet is committed to in the packet which will be stored in the
// packet commitment value
// The final preimage will be prepended by the byte 0x02 before hashing in order to clearly define the protocol version
// and allow for future upgradability
func commitV2Packet(packet: Packet) {
    timeoutBytes = LittleEndian(packet.timeout)
    var appBytes: bytes
    for p in packet.payload {
        appBytes = append(appBytes, commitPayload(p))
    }
    buffer = sha256.Hash(packet.destClient)
    buffer = append(buffer, sha256.hash(timeoutBytes))
    buffer = append(buffer, sha256.hash(appBytes))
    buffer = append([]byte{0x02}, buffer)
    return sha256.Hash(buffer)
}

Acknowledgement V2

第 2 版规范中的确认也经过了修改,以支持一个数据包中包含多个 payload,而这些 payload 会分别发送到不同的应用,并由它们各自写入确认。每个确认都会以与原始数据包中接收顺序相同的顺序包含在最终的数据包确认中。因此,如果一个数据包按顺序包含了发往模块 A 和 B 的 payload;那么接收方写入的确认中,应用确认 A 和 B 也必须保持相同顺序。 确认本身是一个应用确认字节列表,其提交方式必须是对每个单独的确认分别进行哈希、拼接,然后再对结果进行哈希。这可确保所有兼容实现都得到相同的确认承诺,并且两个不同的确认绝不会生成相同的承诺。 应用可能不需要返回确认。在这种情况下,它可以返回一个哨兵确认值 SENTINEL_ACKNOWLEDGMENT,即字节数组中的单个字节:bytes(0x01)。在这种情况下,IBC 的 acknowledgePacket 处理器仍会执行 IBC core 的确认逻辑,但不会调用应用的 acknowledgePacket 回调。
interface Acknowledgement {
    // Each app in the payload will have an acknowledgment in this list in the same order
    // that they were received in the payload
    // If an app does not need to send an acknowledgement, there must be a SENTINEL_ACKNOWLEDGEMENT
    // in its place
    // The app acknowledgement must be encoded in the same manner specified in the payload it received
    // and must be created and processed in the manner expected by the version specified in the payload.
    appAcknowledgement: [bytes]
}
所有确认都必须被提交,并存储在标准化的确认路径下。请注意,由于每个确认都与某个已接收的数据包相关联,因此确认路径使用该数据包的 destClientId 和其 sequence 来构造,从而为该确认生成唯一键。
func acknowledgementPath(packet: Packet) {
    return packet.destClientId + byte(0x02) + bigEndian(packet.Sequence)
}
// commitV2Acknowledgement hashes each app acknowledgment and hashes them together
// the final preimage will be prepended with the byte 0x02 before hashing in order to clearly define the protocol version
// and allow for future upgradability
func commitV2Acknowledgment(ack: Acknowledgement) {
    var buffer: bytes
    for appAck in ack.appAcknowledgement {
        buffer = append(buffer, sha256.Hash(appAck))
    }
    buffer = append([]byte{0x02}, buffer)
    return sha256.Hash(buffer)
}

Packet Receipt V2

数据包收据只会告知发送链,对端已经成功接收了该数据包。因此,我们只需要一个与已发送数据包唯一关联、可证明的布尔标志。因此,接收链会使用目标标识符和序列号作为键来存储数据包收据,以唯一标识该数据包。 对于支持其自身状态不存在性证明的链,它们只需在收据路径下写入一个 SENTINEL_RECEIPT_VALUE。这个 SENTINEL_RECEIPT_PATH 可以是任何非 nil 值,因此建议写入单个字节。收据路径标准如下所示。与确认类似,每个收据都与某个已接收的数据包相关联,因此收据路径使用该数据包的 destClientId 和其 sequence 来构造,从而为该收据生成唯一键。
func receiptPath(packet: Packet) {
    return packet.destClientId + byte(0x03) + bigEndian(packet.Sequence)
}

可证明路径空间

IBC/TAO 实现 MUST 按照下文精确指定的格式,为 provableStore 实现以下路径。这是因为,对手方的 IBC/TAO 实现会依据本规范构造这些路径,并将其发送给轻客户端,以验证存储在 IBC 指定路径下的 IBC 指定值。provableStore 在 ICS24 Host Requirements 中定义。 未来版本的协议可能会使用新的路径,因此 provableStore 中的整个键空间 MUST 为 IBC 处理程序保留。
值路径格式
数据包承诺0x1
数据包回执0x2
确认承诺0x3
请注意,IBC 协议确保数据包的 (sourceClientId, sequence) 元组能在发送链上唯一标识一个数据包,而 (destClientId, sequence) 元组能在接收链上唯一标识一个数据包。该属性与标准化路径中客户端标识符和序列之间的字节分隔符共同确保:对于同一个数据包,承诺、回执和确认会分别写入不同的路径。因此,只要遵循 ICS24 中规定的主机要求,IBC 处理程序为某个给定数据包写入状态的可证明键,就永远不会被不同的值覆盖。这确保了 IBC 生态系统中链间通信的安全性与正确性。

Packet V2 Structure

The IBC packet sends application data from a source chain to a destination chain with a timeout that specifies when the packet is no longer valid. The packet will be committed to by the source chain as specified in the ICS-24 specification. The receiver chain will then verify the packet commitment under the ICS-24 specified packet commitment path. If the proof succeeds, the IBC handler sends the application data(s) to the relevant application(s).
interface Packet {
    // identifier for the destination-chain client existing on source chain
    sourceClientId: bytes,
    // identifier for the source-chain client existing on destination chain
    destClientId: bytes,
    // the sequence uniquely identifies this packet
    // in the stream of packets from source to dest chain
    sequence: uint64,
    // the timeout is the timestamp in seconds on the destination chain
    // at which point the packet is no longer valid.
    // It cannot be received on the destination chain and can
    // be timed out on the source chain
    timeout: uint64,
    // the data includes the messages that are intended
    // to be sent to application(s) on the destination chain
    // from application(s) on the source chain
    // IBC core handlers will route the payload to the desired
    // application using the port identifiers but the rest of the
    // payload will be processed by the application
    data: [Payload]
}

interface Payload {
    // sourcePort identifies the sending application on the source chain
    sourcePort: bytes,
    // destPort identifies the receiving application on the dest chain
    destPort: bytes,
    // version identifies the version that sending application
    // expects destination chain to use in processing the message
    // if dest chain does not support the version, the payload must
    // be rejected with an error acknowledgement
    version: string,
    // encoding allows the sending application to specify which
    // encoding was used to encode the app data
    // the receiving applicaton will decode the appData into
    // the strucure expected given the version provided
    // if the encoding is not supported, receiving application
    // must be rejected with an error acknowledgement.
    // the encoding string MUST be in MIME format
    encoding: string,
    // appData is the opaque content sent from the source application
    // to the dest application. It will be decoded and interpreted
    // as specified by the version and encoding fields
    appData: bytes,
}
The source and destination client identifiers at the top-level of the packet identify the chains communicating. The sourceClientId identifier must be unique on the source chain and is a pointer to the destination chain client on the source chain. The destClientId identifier must be a unique identifier on the destination chain and is a pointer to the source chain client on the destination chain. The sequence is a monotonically incrementing nonce to uniquely identify packets sent between the source and destination chain. The timeout is the UNIX timestamp in seconds that must be passed on the destination chain before the packet is invalid and no longer capable of being received. Note that the timeout timestamp is assessed against the destination chain’s clock which may drift relative to the clocks of the sender chain or a third party observer. If a packet is received on the destination chain after the timeout timestamp has passed relative to the destination chain’s clock; the packet must be rejected so that it can be safely timed out and reverted by the sender chain. In version 2 of the IBC specification, implementations MAY support multiple application data within the same packet. This can be represented by a list of payloads. Implementations may choose to only support a single payload per packet, in which case they can just reject incoming packets sent with multiple payloads. Each payload will include its own Encoding and AppVersion that will be sent to the application to instruct it how to decode and interpret the opaque application data. The application must be able to support the provided Encoding and AppVersion in order to process the AppData. If the receiving application does not support the encoding or app version, then the application must return an error to IBC core. If the receiving application does support the provided encoding and app version, then the application must decode the application as specified by the Encoding string and then process the application as expected by the counterparty given the agreed-upon app version. Since the Encoding and AppVersion are now in each packet they can be changed on a per-packet basis and an application can simultaneously support many encodings and app versions from a counterparty. This is in stark contrast to IBC version 1 where the channel prenegotiated the channel version (which implicitly negotiates the encoding as well); so that changing the app version after channel opening is very difficult. All implementations must commit the packet in the standardized IBC commitment format to satisfy the protocol. In order to do this we must first commit the packet data and timeout. The timeout is encoded in LittleEndian format. The packet data which is a list of payloads is committed to by hashing each individual field of the payload and successively concatenating them together. This ensures a standard unambigious commitment for a given packet. Thus a given packet will always create the exact same commitment by all compliant implementations and two different packets will never create the same commitment by a compliant implementation. This commitment value is then stored under the standardized provable packet commitment key as defined below:
func packetCommitmentPath(packet: Packet): bytes {
    return packet.sourceClientId + byte(0x01) + bigEndian(packet.sequence)
}
// commitPayload hashes all the fields of the packet data to create a standard size
// preimage before committing it in the packet.
func commitPayload(payload: Payload): bytes {
    buffer = sha256.Hash(payload.sourcePort)
    buffer = append(sha256.Hash(payload.destPort))
    buffer = append(sha256.Hash(payload.version))
    buffer = append(sha256.Hash(payload.encoding))
    buffer = append(sha256.Hash(payload.appData))
    return sha256.Hash(buffer)
}

// commitV2Packet commits to all fields in the packet
// by hashing each individual field and then hashing these fields together
// Note: SourceClient and the sequence are omitted since they will be included in the key
// Every other field of the packet is committed to in the packet which will be stored in the
// packet commitment value
// The final preimage will be prepended by the byte 0x02 before hashing in order to clearly define the protocol version
// and allow for future upgradability
func commitV2Packet(packet: Packet) {
    timeoutBytes = LittleEndian(packet.timeout)
    var appBytes: bytes
    for p in packet.payload {
        appBytes = append(appBytes, commitPayload(p))
    }
    buffer = sha256.Hash(packet.destClient)
    buffer = append(buffer, sha256.hash(timeoutBytes))
    buffer = append(buffer, sha256.hash(appBytes))
    buffer = append([]byte{0x02}, buffer)
    return sha256.Hash(buffer)
}

Acknowledgement V2

The acknowledgement in the version 2 specification is also modified to support multiple payloads in the packet that will each go to separate applications that can write their own acknowledgements. Each acknowledgment will be contained within the final packet acknowledgment in the same order that they were received in the original packet. Thus if a packet contains payloads for modules A and B in that order; the receiver will write an acknowledgment with the app acknowledgements A and B in the same order. The acknowledgement which is itself a list of app acknowledgement bytes must be committed to by hashing each individual acknowledgement and concatenating them together and hashing the result. This ensures that all compliant implementations reach the same acknowledgment commitment and that two different acknowledgements never create the same commitment. An application may not need to return an acknowledgment. In this case, it may return a sentinel acknowledgement value SENTINEL_ACKNOWLEDGMENT which will be the single byte in the byte array: bytes(0x01). In this case, the IBC acknowledgePacket handler will still do the core IBC acknowledgment logic but it will not call the application’s acknowledgePacket callback.
interface Acknowledgement {
    // Each app in the payload will have an acknowledgment in this list in the same order
    // that they were received in the payload
    // If an app does not need to send an acknowledgement, there must be a SENTINEL_ACKNOWLEDGEMENT
    // in its place
    // The app acknowledgement must be encoded in the same manner specified in the payload it received
    // and must be created and processed in the manner expected by the version specified in the payload.
    appAcknowledgement: [bytes]
}
All acknowledgements must be committed to and stored under the standardized acknowledgment path. Note that since each acknowledgement is associated with a given received packet, the acnowledgement path is constructed using the packet destClientId and its sequence to generate a unique key for the acknowledgement.
func acknowledgementPath(packet: Packet) {
    return packet.destClientId + byte(0x02) + bigEndian(packet.Sequence)
}
// commitV2Acknowledgement hashes each app acknowledgment and hashes them together
// the final preimage will be prepended with the byte 0x02 before hashing in order to clearly define the protocol version
// and allow for future upgradability
func commitV2Acknowledgment(ack: Acknowledgement) {
    var buffer: bytes
    for appAck in ack.appAcknowledgement {
        buffer = append(buffer, sha256.Hash(appAck))
    }
    buffer = append([]byte{0x02}, buffer)
    return sha256.Hash(buffer)
}

Packet Receipt V2

A packet receipt will only tell the sending chain that the counterparty has successfully received the packet. Thus we just need a provable boolean flag uniquely associated with the sent packet. Thus, the receiver chain stores the packet receipt keyed on the destination identifier and the sequence to uniquely identify the packet. For chains that support nonexistence proofs of their own state, they can simply write a SENTINEL_RECEIPT_VALUE under the receipt path. This SENTINEL_RECEIPT_PATH can be any non-nil value so it is recommended to write a single byte. The receipt path is standardized as below. Similar to the acknowledgement, each receipt is associated with a given received packet the receipt path is constructed using the packet destClientId and its sequence to generate a unique key for the receipt.
func receiptPath(packet: Packet) {
    return packet.destClientId + byte(0x03) + bigEndian(packet.Sequence)
}

Provable Path-space

IBC/TAO implementations MUST implement the following paths for the provableStore in the exact format specified. This is because counterparty IBC/TAO implementations will construct the paths according to this specification and send it to the light client to verify the IBC specified value stored under the IBC specified path. The provableStore is specified in ICS24 Host Requirements Future paths may be used in future versions of the protocol, so the entire key-space in the provable store MUST be reserved for the IBC handler.
ValuePath format
Packet Commitment0x1
Packet Receipt0x2
Acknowledgement Commitment0x3
Note that the IBC protocol ensures that the packet (sourceClientId, sequence) tuple uniquely identifies a packet on the sending chain, and the (destClientId, sequence) tuple uniquely identifies a packet on the receiving chain. This property along with the byte separator between the client identifier and sequence in the standardized paths ensures that commitments, receipts, and acknowledgements are each written to different paths for the same packet. Thus, so long as the host requirements specified in ICS24 are respected; a provable key written to state by the IBC handler for a given packet will never be overwritten with a different value. This ensures secure and correct communication between chains in the IBC ecosystem.