摘要

本标准文档规定了跨链查询模块的数据结构和状态机处理逻辑,该模块允许 IBC 启用链之间进行跨链查询。

概览与基本概念

动机

我们预期链上应用会依赖于从其他链读取的数据,例如,一条链上的某个应用可能需要知道第二条链上某个代币的当前价格。尽管 IBC 协议使链上应用能够与其他链通信,但如果只是用它来查询链状态,成本会过高:这将要求查询链与任意其他链之间维持一个开放通道,并为每一次查询请求使用完整的 IBC 协议栈。注意,后者意味着链之间需要交换数据包,因此被查询链需要提交交易;如果查询请求负载很高,这可能会扰乱其运行。跨链查询解决了这个问题。它使链上应用能够无缝查询其他链的状态:无需被查询链参与,并且对查询链的要求非常低。

定义

Querying chain:希望从另一条链(被查询链)获取数据的链。查询链是实现跨链查询模块的链。 Queried chain:其状态被查询的链。被查询链通过中继器使用其 RPC 客户端进行查询,随后结果会被提交回查询链。 Cross-chain Queries Module:实现跨链查询协议的模块。只有查询链需要集成它。 Height 以及与客户端相关的函数定义见 ICS 2。 newCapability 和 authenticateCapability 定义见 ICS 5。 CommitmentPath 和 CommitmentProof 定义见 ICS 23。 Identifier、get、set、delete、getCurrentHeight 以及与模块系统相关的基础原语定义见 ICS 24。 Fee 定义见 ICS 29。

系统模型与属性

假设

  • 安全链: 查询链和被查询链都是安全的。这意味着对每一条链而言,其底层共识引擎满足安全性(例如,链不会分叉),并且状态机的执行遵循本文描述的协议。
  • 活跃链: 查询链和被查询链都 MUST 保持活跃,也就是说,链上最终会持续产生新区块。
  • 抗审查的查询链: 查询链不能选择性地忽略有效交易。
例如,这意味着如果中继器向查询链提交了一笔有效交易,则该交易最终一定会被包含进某个已提交的区块。注意,Tendermint 当前并不保证这一点。
  • 正确的中继器: 在查询链和被查询链之间,至少存在一个活跃的中继器,并且该中继器会正确遵循协议。
在本规范的语境下,这意味着对于来自查询链的每一个查询请求,至少存在一个中继器会:(i) 获取该查询请求,(ii) 在被查询链上执行查询,以及 (iii) 将结果连同有效证明一起,通过交易提交给查询链。
上述假设足以保证:如果查询链对查询结果进行无界等待,则查询协议会向应用返回结果。尽管如此,本规范考虑的是查询链在固定时间后超时的情况。因此,为了保证查询协议始终向应用返回查询结果,本规范还要求额外假设:查询链以及至少一个正确的中继器都必须及时运行。
  • 及时的查询链: 从交易被提交到该链,到该链提交包含该交易的区块之间,存在一个时间上界。
  • 及时的中继器: 对于正确且活跃的中继器,从中继器获取查询请求到提交查询结果之间,存在一个时间上界。
因此请注意,为了保证查询协议始终向应用返回结果,查询链上的超时上界应至少等于 及时的查询链 和 及时的中继器 两项假设时间上界之和。这样可以保证中继器提交查询结果交易且查询链在规定的超时上界内处理该交易。

期望属性

无许可

查询链可以在无需对方许可的情况下查询另一条链,并且无需任何第三方或链治理批准即可实现跨链查询。注意,由于链之间不存在事先协商,查询链不能假设被查询的数据一定符合预期格式。

最小化被查询链工作量

任何提供查询支持的链都可以作为被查询链,无需额外的实现工作,也不需要任何额外模块。这是通过利用中继器上的 RPC 客户端实现的。

模块化

支持跨链查询应当像在你的链中实现一个模块一样简单。

激励机制

系统会支付赏金,以激励中继器参与跨链查询:从被查询链获取数据,并将其(连同证明)提交给查询链。

技术规范

总体设计

查询链必须实现跨链查询模块,从而允许查询链查询被查询链上的状态。 跨链查询依赖于在两条链之间运行的中继器。当查询链收到查询请求时,跨链查询模块会发出一个 sendQuery 事件。在查询链与被查询链之间运行的中继器必须监听查询链上的 sendQuery 事件。最终,某个中继器会获取该查询请求并在被查询链上执行它,也就是获取数据并生成相应证明。随后,中继器会通过一笔交易将结果提交到查询链。最终,该结果由跨链查询模块在查询链上登记。 查询请求中包含被查询链上必须执行该查询的高度。原因在于,被查询的键在不同高度可能具有不同的值。因此,恶意中继器可能会选择一个对自己有利的高度进行查询。通过让查询链决定查询执行的高度,我们可以防止中继器影响结果数据。
注意,这一机制并不能防止跨链 MEV(最大可提取价值):如果查询高度位于未来,仍然可能通过改变被查询链上的状态来修改查询结果,从而产生可利用空间。

数据结构

跨链查询模块在处理查询请求时会存储这些请求。 CrossChainQuery 是用于表示查询请求的一个特定接口。当其结果被提交时,该请求会被取回。
interface CrossChainQuery {
    id: Identifier
    path: CommitmentPath
    localTimeoutHeight: Height
    localTimeoutTimestamp: uint64
    queryHeight: Height
    clientId: Identifier
    bounty: Fee
}
  • id 字段在查询链上唯一标识该查询。
  • path 字段是被查询链上要查询的路径。
  • localTimeoutHeight 字段指定查询链上的一个高度限制,超过该高度后,该查询会被视为失败,并应向原始调用方返回超时结果。
  • localTimeoutTimestamp 字段指定查询链上的一个时间戳限制,超过该时间戳后,该查询会被视为失败,并应向原始调用方返回超时结果。
  • queryHeight 字段是中继器必须在被查询链上执行查询的高度。
  • clientId 字段标识查询链上对应被查询链的客户端。
  • bounty 字段是给予参与该查询的中继器的赏金。
跨链查询模块会存储查询结果,以便查询调用方能够异步获取这些结果。 在此语境下,本标准将 QueryResult 类型定义如下:
enum QueryResult {
  SUCCESS,
  FAILURE,
  TIMEOUT
}
  • 返回值的查询会被标记为 SUCCESS。这意味着该查询已在被查询链上执行,并且在请求高度下,被查询路径存在对应的值。
  • 已执行但未返回值的查询会被标记为 FAILURE。这意味着该查询已在被查询链上执行,但在请求高度下,被查询路径不存在对应的值。
  • 在结果被提交到查询链之前已经超时的查询会被标记为 TIMEOUT。
CrossChainQueryResult 是用于表示查询结果的一个特定接口。
interface CrossChainQueryResult {
    id: Identifier
    result: QueryResult
    data: []byte
}
  • id 字段在查询链上唯一标识该查询。
  • result 字段表示该查询是否已在被查询链上正确执行,以及被查询路径是否存在。
  • 当 result = SUCCESS 时,data 字段是一个不透明字节串,包含被查询路径关联的值。

存储路径

查询路径

查询路径是一个私有路径,用于存储进行中的跨链查询状态。
function queryPath(id: Identifier): Path {
    return "queries/{id}"
}

结果查询路径

结果查询路径是一个私有路径,用于存储已完成查询的结果。
function queryResultPath(id: Identifier): Path {
    return "result/queries/{id}"
}

辅助函数

查询链 MUST 实现一个 generateIdentifier 函数,用于生成唯一的查询标识符:
function generateIdentifier = () -> Identifier

子协议

查询生命周期

  1. 当查询链收到查询请求时,它会调用跨链查询模块的 CrossChainQueryRequest。该函数会为查询生成唯一标识符,将其存储到 privateStore 中,并发出一个 sendQuery 事件。查询请求既可以作为交易提交到查询链,也可以直接作为 BeginBlock 和 EndBlock 逻辑的一部分执行。通常,查询请求将由其他 IBC 模块发起。
  2. 一个正确的中继器在监听到查询链发出的 sendQuery 事件后,最终会获取该查询请求并在被查询链上执行它。随后,结果会通过交易提交到查询链。
  3. 当查询结果在查询链上被提交后,会调用跨链查询模块的 CrossChainQueryResponse 函数。
  4. CrossChainQueryResponse 首先使用查询的唯一标识符从 privateStore 中取回该查询。然后,它继续使用本地客户端验证结果。如果验证通过,该函数会从 privateStore 中移除该查询,并将结果存入私有存储。
查询链在收到查询结果时,可能会执行额外的状态机逻辑。为将这部分额外状态机逻辑考虑在内,并向查询调用方收取费用,符合本规范的实现可以使用 CrossChainQuery 接口中现有的 bounty 字段,或者为该接口扩展一个额外字段。
  1. 然后,查询调用方可以异步获取查询结果。PruneCrossChainQueryResult 函数允许查询调用方在取回结果后,将其从存储中清除。

常规路径方法

当发起查询的链上的跨链查询模块接收到新的查询请求时,会调用 CrossChainQueryRequest 函数。
function CrossChainQueryRequest(
  path: CommitmentPath,
  queryHeight: Height,
  localTimeoutHeight: Height,
  localTimeoutTimestamp: uint64,
  clientId: Identifier,
  bounty: Fee,
  ): [Identifier, CapabilityKey] {

    // Check that there exists a client of the queried chain. The client will be used to verify the query result.
    abortTransactionUnless(queryClientState(clientId) !== null)

    // Sanity-check that localTimeoutHeight is 0 or greater than the current height, otherwise the query will always time out.
    abortTransactionUnless(localTimeoutHeight === 0 || localTimeoutHeight > getCurrentHeight())
    // Sanity-check that localTimeoutTimestamp is 0 or greater than the current timestamp, otherwise the query will always time out.
    abortTransactionUnless(localTimeoutTimestamp === 0 || localTimeoutTimestamp > currentTimestamp())

    // Generate a unique query identifier.
    queryIdentifier = generateQueryIdentifier()

    // Create a query request record.
    query = CrossChainQuery{queryIdentifier,
                            path,
                            queryHeight,
                            localTimeoutHeight,
                            localTimeoutTimestamp, 
                            clientId,
                            bounty}

    // Store the query in the local, private store.
    privateStore.set(queryPath(queryIdentifier), query)

    queryCapability = newCapability(queryIdentifier)

    // Log the query request.
    emitLogEntry("sendQuery", query)

    // Returns the query identifier.
    return [queryIdentifier, queryCapability]
}
  • 前置条件
    • 存在一个标识符为 clientId 的客户端。
  • 后置条件
    • 查询请求被存储在 privateStore 中。
    • 发出一个 sendQuery 事件。
当发起查询的链上的跨链查询模块接收到新的查询回复时,会调用 CrossChainQueryResponse 函数。 我们传入将查询结果提交到发起查询链上的中继者地址,以便按需提供一些奖励。这为费用支付提供了基础,也可以用于其他机制(例如计算排行榜)。
function CrossChainQueryResponse(
  queryId: Identifier,
  data: []byte
  proof: CommitmentProof,
  proofHeight: Height,
  delayPeriodTime: uint64,
  delayPeriodBlocks: uint64,
  relayer: string
  ) {

    // Retrieve query state from the local, private store using the query's identifier.
    query = privateStore.get(queryPath(queryIdentifier))
    abortTransactionUnless(query !== null)

    // Retrieve client state of the queried chain.
    clientState = queryClientState(query.clientId)
    abortTransactionUnless(client !== null)

    // Check that the relier executed the query at the requested height at the queried chain.
    abortTransactionUnless(query.queryHeight !== proofHeight)

    // Check that localTimeoutHeight is 0 or greater than the current height.
    abortTransactionUnless(query.localTimeoutHeight === 0 || query.localTimeoutHeight > getCurrentHeight())
    // Check that localTimeoutTimestamp is 0 or greater than the current timestamp.
    abortTransactionUnless(query.localTimeoutTimestamp === 0 || query.localTimeoutTimestamp > currentTimestamp()) 


    // Verify query result using the local light client of the queried chain.
    // If the response carries data, then verify that the data is indeed the value associated with query.path at query.queryHeight at the queried chain.
    if (data !== null) {    
        abortTransactionUnless(verifyMembership(
            clientState,
            proofHeight,
            delayPeriodTime,
            delayPeriodBlocks,
            proof,
            query.path,
            data
        ))
        result = SUCCESS
    // If the response does not carry any data, verify that query.path does not exist at query.queryHeight at the queried chain.
    } else {
        abortTransactionUnless(verifyNonMembership(
            clientState,
            proofHeight,
            delayPeriodTime,
            delayPeriodBlocks,
            proof,
            query.path,
        ))
        result = FAILURE
    }

    // Delete the query from the local, private store.
    privateStore.delete(queryPath(queryId))

    // Create a query result record.
    resultRecord = CrossChainQuery{queryIdentifier,
                                   result,
                                   data} 

    // Store the result in the local, private store.
    privateStore.set(queryResultPath(queryIdentifier), resultRecord)

}
  • 前置条件
    • 存在一个标识符为 clientId 的客户端。
    • 在 privateStore 中存在一个由 queryId 标识的查询请求。
  • 后置条件
    • 由 queryId 标识的查询请求会从 privateStore 中删除。
    • 查询结果会存储在 privateStore 中。
当查询的调用方已经获取结果并希望将其删除时,会调用 PruneCrossChainQueryResult 函数。
function PruneCrossChainQueryResult(
  queryId: Identifier,
  queryCapability: CapabilityKey
  ) {

    // Retrieve the query result from the private store using the query's identifier.
    resultRecord = privateStore.get(queryResultPath(queryIdentifier))
    abortTransactionUnless(resultRecord !== null)

    // Abort the transaction unless the caller has the right to clean the query result
    abortTransactionUnless(authenticateCapability(queryId, queryCapability))

    // Delete the query result from the local, private store.
    privateStore.delete(queryResultPath(queryId))
}
  • 前置条件
    • 在 privateStore 中存在一个由 queryId 标识的查询结果。
    • 调用方有权清理该查询结果。
  • 后置条件
    • 由 queryId 标识的查询结果会从 privateStore 中删除。

超时

查询请求带有关联的 localTimeoutHeight 和 localTimeoutTimestamp 字段,用于指定发起查询链上的高度和时间戳上限;超过该上限后,查询会被视为失败。 关于如何处理超时,有多种方案。例如,中继者可以将超时通知作为交易提交给发起查询的链。由于中继者不受信任,对于每一个此类通知,发起查询链上的跨链查询模块都必须调用 checkQueryTimeout 来检查该查询是否确实已经超时。另一种方案是由跨链查询模块负责在每个区块开始时遍历进行中的查询并调用 checkQueryTimeout,以检查是否有查询已超时。在这种情况下,进行中的查询应按 localTimeoutTimestamp 和 localTimeoutHeight 建立索引存储,以便更高效地遍历。这些都属于实现细节,不在本规范覆盖范围内。 假设由中继者负责将超时通知作为交易提交。checkQueryTimeout 函数如下所示。注意, 这里和 CrossChainQueryResponse 一样传入中继者地址,以便同样支持可能的激励机制。
function checkQueryTimeout(
    queryId: Identifier,
    relayer: string
){
    // Retrieve the query state from the local, private store using the query's identifier.
    query = privateStore.get(queryPath(queryIdentifier))
    abortTransactionUnless(query !== null)

    // Get the current height.
    currentHeight = getCurrentHeight()

    // Check that localTimeoutHeight or localTimeoutTimestamp has passed on the querying chain (locally)
    abortTransactionUnless(
      (query.localTimeoutHeight > 0 && query.localTimeoutHeight < getCurrentHeight()) ||
      (query.localTimeoutTimestamp > 0 && query.localTimeoutTimestamp < currentTimestamp()))

    // Delete the query from the local, private store if it has timed out
    privateStore.delete(queryPath(queryId))

    // Create a query result record.
    resultRecord = CrossChainQuery{queryIdentifier,
                                   TIMEOUT,
                                   query.caller
                                   null} 

    // Store the result in the local, private store.
    privateStore.set(resultQueryPath(queryIdentifier), resultRecord)
}
  • 前置条件
    • 在 privateStore 中存在一个由 queryId 标识的查询请求。
  • 后置条件
    • 如果查询确实已经超时,则:
      • 由 queryId 标识的查询请求会从 privateStore 中删除;
      • 查询已超时这一事实会记录到 privateStore 中。

历史

2022 年 1 月 6 日 - 首个草案 2022 年 5 月 11 日 - 重大修订 2022 年 6 月 14 日 - 增加清理机制、localTimeoutTimestamp,并加入用于激励的中继者地址 2022 年 7 月 28 日 - 对假设进行修订

版权

此处所有内容均基于 Apache 2.0 许可。

Synopsis

This standard document specifies the data structures and state machine handling logic of the Cross-chain Queries module, which allows for cross-chain querying between IBC enabled chains.

Overview and Basic Concepts

Motivation

We expect on-chain applications to depend on reads from other chains, e.g., a particular application on a chain may need to know the current price of the token of a second chain. While the IBC protocol enables on-chain applications to talk to other chains, using it for simply querying the state of chains would be too expensive: it would require to maintain an open channel between the querying chain and any other chain, and use the full IBC stack for every query request. Note that the latter implies exchanging packets between chains and therefore committing transactions at the queried chain, which may disrupt its operation if the load of query requests is high. Cross-chain queries solve this issue. It enables on-chain applications to query the state of other chains seamlessly: without involving the queried chain, and requiring very little from the querying chain.

Definitions

Querying chain: The chain that is interested in getting data from another chain (queried chain). The querying chain is the chain that implements the Cross-chain Queries module. Queried chain: The chain whose state is being queried. The queried chain gets queried via a relayer utilizing its RPC client which is then submitted back to the querying chain. Cross-chain Queries Module: The module that implements the cross-chain querying protocol. Only the querying chain integrates it. Height and client-related functions are as defined in ICS 2. newCapability and authenticateCapability are as defined in ICS 5. CommitmentPath and CommitmentProof are as defined in ICS 23. Identifier, get, set, delete, getCurrentHeight, and module-system related primitives are as defined in ICS 24. Fee is as defined in ICS 29.

System Model and Properties

Assumptions

  • Safe chains: Both the querying and queried chains are safe. This means that, for every chain, the underlying consensus engine satisfies safety (e.g., the chain does not fork) and the execution of the state machine follows the described protocol.
  • Live chains: Both the querying and queried chains MUST be live, i.e., new blocks are eventually added to the chain.
  • Censorship-resistant querying chain: The querying chain cannot selectively omit valid transactions.
For example, this means that if a relayer submits a valid transaction to the querying chain, the transaction is guaranteed to be eventually included in a committed block. Note that Tendermint does not currently guarantee this.
  • Correct relayer: There is at least one live relayer between the querying and queried chains where the relayer correctly follows the protocol.
In the context of this specification, this implies that for every query request coming from the querying chain, there is at least one relayer that (i) picks the query request up, (ii) executes the query at the queried chain, and (iii) submits the result in a transaction, together with a valid proof, to the querying chain.
The above assumptions are enough to guarantee that the query protocol returns results to the application if the querying chain waits unboundly for query results. Nevertheless, this specification considers the case when the querying chain times out after a fixed period of time. Thus, to guarantee that the query protocol always returns query results to the application, the specification requires additional assumptions: both the querying chain and at least one correct relayer have to behave timely.
  • Timely querying chain: There exists an upper-bound in the time elapsed between the moment a transaction is submitted to the chain and when the chain commits a block including it.
  • Timely relayer: For correct and live relayers, there exists an upper-bound in the time elapsed between the moment a relayer picks a query request and when the relayer submits the query result.
Note then that to guarantee that the query protocol always returns results to the application, the timeout bound at the querying chain should be at least equal to the sum of the upper-bounds of assumptions Timely querying chain and Timely relayer. This would guarantee that the relayer submits and the querying chain process a query result transaction within the specified timeout bound.

Desired Properties

Permissionless

The querying chain can query a chain without permission from the latter and implement cross-chain querying without any approval from a third party or chain governance. Note that since there is no prior negotiation between chains, the querying chain cannot assume that queried data will be in an expected format.

Minimal queried chain Work

Any chain that provides query support can act as a queried chain, requiring no implementation work or any extra module. This is possible by utilizing an RPC client on a relayer.

Modular

Supporting cross-chain queries should be as easy as implementing a module in your chain.

Incentivization

A bounty is paid to incentivize relayers for participating in cross-chain queries: fetching data from the queried chain and submitting it (together with proofs) to the querying chain.

Technical Specification

General Design

The querying chain must implement the Cross-chain Queries module, which allows the querying chain to query state at the queried chain. Cross-chain queries rely on relayers operating between both chains. When a query request is received by the querying chain, the Cross-chain Queries module emits a sendQuery event. Relayers operating between the querying and queried chains must monitor the querying chain for sendQuery events. Eventually, a relayer will retrieve the query request and execute it, i.e., fetch the data and generate the corresponding proofs, at the queried chain. The relayer then submits the result in a transaction to the querying chain. The result is finally registered at the querying chain by the Cross-chain Queries module. A query request includes the height of the queried chain at which the query must be executed. The reason is that the keys being queried can have different values at different heights. Thus, a malicious relayer could choose to query a height that has a value that benefits it somehow. By letting the querying chain decide the height at which the query is executed, we can prevent relayers from affecting the result data.
Note that this mechanism does not prevent cross-chain MEV (maximal extractable value): this still creates an opportunity for altering the state on the queried chain if the height is in the future in order to change the results of the query.

Data Structures

The Cross-chain Queries module stores query requests when it processes them. A CrossChainQuery is a particular interface to represent query requests. A request is retrieved when its result is submitted.
interface CrossChainQuery {
    id: Identifier
    path: CommitmentPath
    localTimeoutHeight: Height
    localTimeoutTimestamp: uint64
    queryHeight: Height
    clientId: Identifier
    bounty: Fee
}
  • The id field uniquely identifies the query at the querying chain.
  • The path field is the path to be queried at the queried chain.
  • The localTimeoutHeight field specifies a height limit at the querying chain after which a query is considered to have failed and a timeout result should be returned to the original caller.
  • The localTimeoutTimestamp field specifies a timestamp limit at the querying chain after which a query is considered to have failed and a timeout result should be returned to the original caller.
  • The queryHeight field is the height at which the relayer must query the queried chain
  • The clientId field identifies the querying chain’s client of the queried chain.
  • The bounty field is a bounty that is given to the relayer for participating in the query.
The Cross-chain Queries module stores query results to allow query callers to asynchronously retrieve them. In this context, this standard defines the QueryResult type as follows:
enum QueryResult {
  SUCCESS,
  FAILURE,
  TIMEOUT
}
  • A query that returns a value is marked as SUCCESS. This means that the query has been executed at the queried chain and there was a value associated to the queried path at the requested height.
  • A query that is executed but does not return a value is marked as FAILURE. This means that the query has been executed at the queried chain, but there was no value associated to the queried path at the requested height.
  • A query that timed out before a result is committed at the querying chain is marked as TIMEOUT.
A CrossChainQueryResult is a particular interface used to represent query results.
interface CrossChainQueryResult {
    id: Identifier
    result: QueryResult
    data: []byte
}
  • The id field uniquely identifies the query at the querying chain.
  • The result field indicates whether the query was correctly executed at the queried chain and if the queried path exists.
  • The data field is an opaque bytestring that contains the value associated with the queried path in case result = SUCCESS.

Store paths

Query path

The query path is a private path that stores the state of ongoing cross-chain queries.
function queryPath(id: Identifier): Path {
    return "queries/{id}"
}

Result query path

The result query path is a private path that stores the result of completed queries.
function queryResultPath(id: Identifier): Path {
    return "result/queries/{id}"
}

Helper functions

The querying chain MUST implement a function generateIdentifier, which generates a unique query identifier:
function generateIdentifier = () -> Identifier

Sub-protocols

Query lifecycle

  1. When the querying chain receives a query request, it calls CrossChainQueryRequest of the Cross-chain Queries module. This function generates a unique identifier for the query, stores it in its privateStore and emits a sendQuery event. Query requests can be submitted as transactions to the querying chain or simply executed as part of the BeginBlock and EndBlock logic. Typically, query requests will be issued by other IBC modules.
  2. A correct relayer listening to sendQuery events from the querying chain will eventually pick the query request up and execute it at the queried chain. The result is then submitted in a transaction to the querying chain.
  3. When the query result is committed at the querying chain, this calls the CrossChainQueryResponse function of the Cross-chain Queries module.
  4. The CrossChainQueryResponse first retrieves the query from the privateStore using the query’s unique identifier. It then proceeds to verify the result using its local client. If it passes the verification, the function removes the query from the privateStore and stores the result in the private store.
The querying chain may execute additional state machine logic when a query result is received. To account for this additional state machine logic and charge a fee to the query caller, an implementation of this specification could use the already existing bounty field of the CrossChainQuery interface or extend the interface with an additional field.
  1. The query caller can then asynchronously retrieve the query result. The function PruneCrossChainQueryResult allows a query caller to prune the result from the store once it retrieves it.

Normal path methods

The CrossChainQueryRequest function is called when the Cross-chain Queries module at the querying chain receives a new query request.
function CrossChainQueryRequest(
  path: CommitmentPath,
  queryHeight: Height,
  localTimeoutHeight: Height,
  localTimeoutTimestamp: uint64,
  clientId: Identifier,
  bounty: Fee,
  ): [Identifier, CapabilityKey] {

    // Check that there exists a client of the queried chain. The client will be used to verify the query result.
    abortTransactionUnless(queryClientState(clientId) !== null)

    // Sanity-check that localTimeoutHeight is 0 or greater than the current height, otherwise the query will always time out.
    abortTransactionUnless(localTimeoutHeight === 0 || localTimeoutHeight > getCurrentHeight())
    // Sanity-check that localTimeoutTimestamp is 0 or greater than the current timestamp, otherwise the query will always time out.
    abortTransactionUnless(localTimeoutTimestamp === 0 || localTimeoutTimestamp > currentTimestamp())

    // Generate a unique query identifier.
    queryIdentifier = generateQueryIdentifier()

    // Create a query request record.
    query = CrossChainQuery{queryIdentifier,
                            path,
                            queryHeight,
                            localTimeoutHeight,
                            localTimeoutTimestamp, 
                            clientId,
                            bounty}

    // Store the query in the local, private store.
    privateStore.set(queryPath(queryIdentifier), query)

    queryCapability = newCapability(queryIdentifier)

    // Log the query request.
    emitLogEntry("sendQuery", query)

    // Returns the query identifier.
    return [queryIdentifier, queryCapability]
}
  • Precondition
    • There exists a client with clientId identifier.
  • Postcondition
    • The query request is stored in the privateStore.
    • A sendQuery event is emitted.
The CrossChainQueryResponse function is called when the Cross-chain Queries module at the querying chain receives a new query reply. We pass the address of the relayer that submitted the query result to the querying chain to optionally provide some rewards. This provides a foundation for fee payment, but can be used for other techniques as well (like calculating a leaderboard).
function CrossChainQueryResponse(
  queryId: Identifier,
  data: []byte
  proof: CommitmentProof,
  proofHeight: Height,
  delayPeriodTime: uint64,
  delayPeriodBlocks: uint64,
  relayer: string
  ) {

    // Retrieve query state from the local, private store using the query's identifier.
    query = privateStore.get(queryPath(queryIdentifier))
    abortTransactionUnless(query !== null)

    // Retrieve client state of the queried chain.
    clientState = queryClientState(query.clientId)
    abortTransactionUnless(client !== null)

    // Check that the relier executed the query at the requested height at the queried chain.
    abortTransactionUnless(query.queryHeight !== proofHeight)

    // Check that localTimeoutHeight is 0 or greater than the current height.
    abortTransactionUnless(query.localTimeoutHeight === 0 || query.localTimeoutHeight > getCurrentHeight())
    // Check that localTimeoutTimestamp is 0 or greater than the current timestamp.
    abortTransactionUnless(query.localTimeoutTimestamp === 0 || query.localTimeoutTimestamp > currentTimestamp()) 


    // Verify query result using the local light client of the queried chain.
    // If the response carries data, then verify that the data is indeed the value associated with query.path at query.queryHeight at the queried chain.
    if (data !== null) {    
        abortTransactionUnless(verifyMembership(
            clientState,
            proofHeight,
            delayPeriodTime,
            delayPeriodBlocks,
            proof,
            query.path,
            data
        ))
        result = SUCCESS
    // If the response does not carry any data, verify that query.path does not exist at query.queryHeight at the queried chain.
    } else {
        abortTransactionUnless(verifyNonMembership(
            clientState,
            proofHeight,
            delayPeriodTime,
            delayPeriodBlocks,
            proof,
            query.path,
        ))
        result = FAILURE
    }

    // Delete the query from the local, private store.
    privateStore.delete(queryPath(queryId))

    // Create a query result record.
    resultRecord = CrossChainQuery{queryIdentifier,
                                   result,
                                   data} 

    // Store the result in the local, private store.
    privateStore.set(queryResultPath(queryIdentifier), resultRecord)

}
  • Precondition
    • There exists a client with clientId identifier.
    • There is a query request stored in the privateStore identified by queryId.
  • Postcondition
    • The query request identified by queryId is deleted from the privateStore.
    • The query result is stored in the privateStore.
The PruneCrossChainQueryResult function is called when the caller of a query has retrieved the result and wants to delete it.
function PruneCrossChainQueryResult(
  queryId: Identifier,
  queryCapability: CapabilityKey
  ) {

    // Retrieve the query result from the private store using the query's identifier.
    resultRecord = privateStore.get(queryResultPath(queryIdentifier))
    abortTransactionUnless(resultRecord !== null)

    // Abort the transaction unless the caller has the right to clean the query result
    abortTransactionUnless(authenticateCapability(queryId, queryCapability))

    // Delete the query result from the local, private store.
    privateStore.delete(queryResultPath(queryId))
}
  • Precondition
    • There is a query result stored in the privateStore identified by queryId.
    • The caller has the right to clean the query result
  • Postcondition
    • The query result identified by queryId is deleted from the privateStore.

Timeouts

Query requests have associated a localTimeoutHeight and a localTimeoutTimestamp field that specifies the height and timestamp limit at the querying chain after which a query is considered to have failed. There are several alternatives on how to handle timeouts. For instance, the relayer could submit timeout notifications as transactions to the querying chain. Since the relayer is untrusted, for each of these notifications, the Cross-chain Queries module of the querying chain MUST call the checkQueryTimeout to check if the query has indeed timed out. An alternative could be to make the Cross-chain Queries module responsible for checking if any query has timed out by iterating over the ongoing queries at the beginning of a block and calling checkQueryTimeout. In this case, ongoing queries should be stored indexed by localTimeoutTimestamp and localTimeoutHeight to allow iterating over them more efficiently. These are implementation details that this specification does not cover. Assume that the relayer is in charge of submitting timeout notifications as transactions. The checkQueryTimeout function would look as follows. Note that we pass the relayer address just as in CrossChainQueryResponse to allow for possible incentivization here as well.
function checkQueryTimeout(
    queryId: Identifier,
    relayer: string
){
    // Retrieve the query state from the local, private store using the query's identifier.
    query = privateStore.get(queryPath(queryIdentifier))
    abortTransactionUnless(query !== null)

    // Get the current height.
    currentHeight = getCurrentHeight()

    // Check that localTimeoutHeight or localTimeoutTimestamp has passed on the querying chain (locally)
    abortTransactionUnless(
      (query.localTimeoutHeight > 0 && query.localTimeoutHeight < getCurrentHeight()) ||
      (query.localTimeoutTimestamp > 0 && query.localTimeoutTimestamp < currentTimestamp()))

    // Delete the query from the local, private store if it has timed out
    privateStore.delete(queryPath(queryId))

    // Create a query result record.
    resultRecord = CrossChainQuery{queryIdentifier,
                                   TIMEOUT,
                                   query.caller
                                   null} 

    // Store the result in the local, private store.
    privateStore.set(resultQueryPath(queryIdentifier), resultRecord)
}
  • Precondition
    • There is a query request stored in the privateStore identified by queryId.
  • Postcondition
    • If the query has indeed timed out, then
      • the query request identified by queryId is deleted from the privateStore;
      • the fact that the query has timed out is recorded in the privateStore.

History

January 6, 2022 - First draft May 11, 2022 - Major revision June 14, 2022 - Adds pruning, localTimeoutTimestamp and adds relayer address for incentivization July 28, 2022 - Revision of the assumptions All content herein is licensed under Apache 2.0.