跨链查询(CCQ)

本文档描述了跨链查询(CCQ)的设计方案,旨在提供一种机制,让集成者可以请求信息并从守护者那里获得关于他们所连接链的证明。CCQ 允许链上和链下发起信息请求,支持 EVM 链和 Solana 链,通过 REST API 接收和执行请求,并提供 Typescript 库和 EVM 响应解析库来辅助集成。同时,文章还讨论了安全性和部署方面的考虑。

跨链查询 (CCQ)

目标

为集成者提供一种机制,用于请求信息并从与其连接的链的守护者那里接收证明。

背景

截至 2023 年 10 月,Wormhole 目前仅支持“推送”证明。 例如,为了获取以太坊上某个合约的状态,需要编写智能合约并将其部署到以太坊,明确读取该状态并在核心桥上调用 publishMessage。 此外,每次在其他地方需要数据时,都需要进行成本高昂且耗时的交易。

此设计提出了一种“拉取”证明的机制。 希望能够在链上或链下发起这些请求。 但是,CCQ 的初始版本仅支持链下请求。

目标

  • 建立一种机制,用于在链上和链下发起请求
  • 提供一种响应请求的解决方案
  • 提供一种通用的、可扩展的解决方案来描述请求和响应
  • 提供一种请求重放保护形式
  • 提供一种 DoS 缓解形式
  • 提供一种序列化格式
  • 支持批量查询请求

非目标

  • 查询响应的数据可用性(响应不会被持久化)
  • 描述所有可能的特定于实现的查询请求或响应格式
  • 证明结果的最终状态
  • 在查询前将标签(latestsafefinalized 等)解析为特定的区块哈希或编号
  • 中继查询响应

概述

Wormhole 守护者运行许多连接链的完整节点。 在当前设计中,如果希望从其中一个链上的另一个链使用任何信息,则必须由数据所在的链上的专门开发的合约“推送”。 这会导致交易延迟,以及在该链上执行该交易的成本。 对于可能只需要按需跨链证明状态更改的应用程序,始终发布消息的额外复杂性和成本效率低下。

考虑一下使用跨链查询后,来自以太坊的 Token Bridge 的 token 证明会有何不同。 无需进行以太坊交易到 token bridge 以调用给定 tokenAddressdecimals()symbol()name(),并等待该交易的最终确认,而是可以通过守护者对以太坊上的该合约进行这三个调用的跨链查询。 跨链查询可能明显更快(可能只需几秒而不是 15-20 分钟),并且避免了在以太坊上支付 gas 的需要。

CCQ 功能增加了集成者向守护者网络提交查询请求并接收来自守护者的经过认证的响应的能力。 守护者监听这些请求,将它们提交到适当的链,从链接收结果,然后将它们发布回 gossip 网络。 然后,集成者可以累积这些请求并关联结果(应用法定人数),并确保结果的准确性。

详细设计

守护者节点是 CCQ 功能的核心。 它们将负责接收来自集成者的请求(链上和链下),针对相应的 RPC 节点执行请求,并返回结果。

此外,为了方便链下请求,将存在一个 CCQ 查询服务器,该服务器将提供一个 REST 端点来接收和执行针对守护者网络的请求。 可以有多个 CCQ 查询服务器,每个服务器都有自己的 peer ID 和签名钱包,这些签名钱包需要在守护者上进行允许列表。

下图显示了基于浏览器的应用程序的查询请求的基本流程。

守护者服务器

CCQ 将作为 guardiand 中的一个可选组件运行。 如果未配置,则不会启用。 守护者将独立处理查询请求。 与观察不同,守护者不会监听或处理来自彼此的 CCQ 流量。 每个守护者将收到请求,对其进行处理,如果有效,则执行查询并发布结果。 如果请求无效,守护者将直接丢弃该请求而不做出响应。

监听请求

请求格式是可扩展的,以便支持跨异构链查询数据和批量处理请求,以最大限度地减少 gossip 流量和 RPC 开销。

截至 2024 年 2 月,CCQ 的当前版本支持 EVM 链和 Solana。 但是,该软件可以扩展到其他链,例如 CosmWasm 等。

链下请求

守护者将通过 P2P gossip 监听链下请求。 为了最大限度地减少开销,CCQ 将使用与现有 gossip 分开的 P2P 信道。 这样,任何未启用 CCQ 的守护者,以及监听 gossip 的其他应用程序(例如 spies 和 flies)都不会看到任何 CCQ 流量。

CCQ 信道将使用两个主题,ccq_reqccq_resp。 守护者将订阅两者,但仅加入 ccq_req。 这意味着守护者将看到所有请求,但看不到任何响应。

守护者将使用 P2P WithPeerFilterRegisterTopicValidator 过滤器来仅接受来自允许列表中的 peer 的 P2P 流量。

守护者将监听 ccq_req 主题上的请求。 收到请求后,将将其提交到查询模块进行处理。

链上请求

CCQ 的初始版本将不支持链上请求。

可以通过新的跨链查询合约在受支持的链上发起请求。 该合约可以构造一个表示请求者和请求的 payload,并通过核心桥发布它,从而生成标准的 VAA。 守护者可以有一个预定义的这些发射器列表,以将其视为跨链查询请求并相应地处理这些请求。

请求验证

查询模块将执行请求验证,包括以下内容:

  • 验证签名。 这包括验证请求是否使用正确的前缀签名,以便不会重放来自其他环境的请求(例如,不能在 mainnet 中播放 testnet 消息)。
  • 验证签名者是否被允许执行 CCQ 请求(是否在允许列表中)。
  • 验证查询中包含的所有每个链的请求是否有效。
eth_call 查询中的区块 ID

请注意,对于 eth_call 查询,block_id 必须是区块号或区块哈希。 不支持 latestfinalized 等标签。 这是因为不同的守护者对于 latestfinalized 可能具有不同的值,具体取决于其节点的状态。

请注意,可能需要支持使用 latestfinalized 等标签,这可能需要 gossip 区块号或让查询服务器读取数据。 这将作为后续功能处理。

eth_call_by_timestamp 中的时间戳和区块 ID 提示

请注意,对于 eth_call_by_timestamp 查询,必须指定 timestamp。 此外,可以指定 hint_target_block_idhint_following_block_id 以帮助守护者确定查询范围。 请注意,如果指定了其中一个,则必须同时指定两者。 提示的格式与 eth_call 中的 block_id 相同。

eth_call_with_finality 中的期望最终性

请注意,对于 eth_call_with_finality 查询,必须指定 finality。 唯一有效的值是 finalizedsafeblock_id 是必需的,并且具有与 eth_call 中相同的格式。

签名验证

请求消息必须在 payload 中包含签名,以便区分请求者和(可能,第三方)p2p 中继器。 该签名应该使用单独的密钥创建。

为了区分签名并防止重放攻击,即从 mainnet 重放用于 devnet/testnet 的请求,必须在签名时使用以下前缀。 这些前缀字符串填充为 35 字节,与 守护者密钥用法 和现有代码保持一致。

mainnet_query_request_000000000000|
testnet_query_request_000000000000|
devnet_query_request_0000000000000|
Solana 支持

截至 2024 年 1 月,正在添加 Solana 查询的实验性实现。 此实现被认为是实验性的,因为 Solana 本机不支持读取特定 slot number 的帐户数据,这意味着每个 guardiand 观察者将返回其最新 slot 版本的数据,这可能使其难以达成共识。 该计划是将其部署到 mainnet,以便我们可以尝试各种方法来实现共识。

请求执行

验证请求后,查询模块会将各个每个链的查询请求提交到相应的观察者以供执行。 观察者会将 RPC 调用提交到节点并返回结果。 查询模块和观察者之间的通信是通过每个观察者的一对 golang 信道进行的,一个入站和一个出站。 观察者将使用批量请求来最大限度地减少 RPC 开销。 为了使此方法有效,集成者应将 RPC 调用正确分组到最少的每个链查询集中。

对于 eth_call_with_finality 请求,观察者将不会返回结果,直到请求的区块达到所需的最终性级别。 此外,在不发布安全区块的链上,对最终性 "safe" 的请求将被视为 "finalized",而不是引发错误。

查询模块将侦听所有每个链查询的响应。 收到所有每个链的响应后,该模块将发布结果以在 gossip 网络上发布。 如果任何响应失败或超时,查询模块将定期重试最多一分钟。 如果在一分钟后某些每个链的查询未成功,则查询模块将放弃该请求。

请注意,守护者不会响应错误的请求,以最大限度地减少 DoS 攻击媒介。 如果他们确实响应,恶意用户可能会用错误的请求轰炸 gossip 网络,这将导致每个请求的众多错误响应成倍增加。 CCQ 查询服务器会请求验证,并在检测到错误的请求时以错误响应。

发布响应

当且仅当请求已成功处理时,守护者才将使用 ccq_resp 主题通过 P2P 发布查询响应消息。 如前所述,其他守护者不会看到此消息。 只有 REST 服务器以及可能的第三方集成者会看到它。

查询响应包含初始查询请求和结果。 请求的存在允许集成者验证响应是否是他们期望的。

响应应使用前缀 query_response_0000000000000000000| 进行签名。 请注意,没有必要为每个环境使用不同的响应前缀,因为响应使用守护者密钥签名,该密钥在不同环境之间是不同的。

守护者配置

CCQ 的守护者配置将包含以下配置参数。

  • ccqEnabled - 如果设置为 true,则启用 CCQ 功能。 默认为 false。
  • ccqAllowedRequesters - 允许提交查询请求的签名者公钥的逗号分隔列表。 没有默认值。
  • ccqP2pPort - 用于绑定 CCQ P2P 信道的本地端口,默认为 8996
  • ccqP2pBootstrap - CCQ P2P 信道的引导 peer。 没有默认值(但在 tilt 中自动生成)。
  • ccqAllowedPeers - 允许提交查询请求的 P2P peer ID 的逗号分隔列表。

守护者中没有查询持久性

为了减轻守护者节点的存储负担,完整的响应不会持久存储在守护者中。 但是,为了方便去重和授权,一些跨链查询信息可能会提交到网关 (Wormchain)。

CCQ REST 服务器

一些集成者可能会选择通过编码自己的查询请求并将其发布到 gossip 网络来直接与守护者通信。 这样做将需要在其 P2P peer ID 在守护者上进行配置。 此外,他们需要有一个 P2P 库,这限制了他们的选择(例如,目前没有用于 P2P 的 typescript 库)。 此外,集成者需要收集和整合响应。

由于这些和其他原因,正在提供一个单独的 REST 服务器来接受链下查询请求,将其提交到 gossip 网络,关联响应,并将其返回给调用者。

CCQ 服务器将配置为在 CCQ P2P 网络上发布和侦听。 它的 P2P peer ID 将在守护者处注册。 服务器将同时加入 ccq_reqccq_resp,但仅订阅 ccq_resp

服务器将配置为支持的用户列表。 每个用户将具有一个 API 密钥,以及他们被允许执行的查询列表。 每个用户的查询数据将如下所示:

{
    "userName": "Test User",
    "apiKey": "432185ac-fbb7-4883-b68d-888e7bd749a9",
    "allowUnsigned": true,
    "allowedCalls": [
    {
        "ethCall": {
        "note:": "Name of WETH on Goerli",
        "chain": 2,
        "contractAddress": "B4FBF271143F4FBf7B91A5ded31805e42b2208d6",
        "call": "0x06fdde03"
        }
    }
    ]
}

当用户提交查询时,他们需要在请求中设置 X-API-Key 标头。

服务器将仅接受来自已配置用户的请求。 这些请求可能仅用于 allowedCalls 列表中配置的 RPC 调用。

每个允许的调用由以下指定:

  • 目标 Wormhole 链 ID
  • 该链上的目标合约地址
  • 要调用的方法签名的哈希值的前四个字节。

所有配置的用户都可以提交他们使用自己的密钥签名的查询。 除了签名请求外,如果 allowUnsigned 标志设置为 true,则用户可以提交未签名的请求,服务器将使用预配置的密钥对其进行签名。 请注意,所有密钥必须在守护者允许列表中。

Typescript 库

为了帮助集成者,sdk/js-query 中有一个 typescript 库,该库提供了用于构建查询的方法。 此外,sdk/js-query/src/query/ethCall.test.ts 中有一些用法示例。

EVM 响应解析库

收到响应后,集成者需要验证和解析它以获取结果。 有一个简单的 EVM 库可以简化此过程,该库可以在 ethereum/contracts/query/QueryResponse.sol 中找到。 一个用法示例在 ethereum/forge-test/query/Query.t.sol 中。

核心桥优化

QueryResponse 解析库使用 Wormhole Core 合约来验证响应上的签名。 这目前涉及进行四个跨合约调用,从而浪费 gas。 一个理想的优化是向 Wormhole 合约添加一个新的公共方法,该方法将采用 payload(响应字节)和签名集,并在一次调用中执行验证。 如果签名无效,它应该回滚。

规范

查询请求

链下

u8       version
u32      nonce
u8       num_per_chain_queries
[]byte   per_chain_queries

每个链的查询

可以在单个 Per-Chain Query 中提交同一链的多个查询。

u16      chain_id
u8       type
uint32   query_len
[]byte   query_data
EVM 查询

目前,EVM 上支持的查询类型是 eth_calleth_call_by_timestampeth_call_with_finality。 这可以扩展以支持其他协议。

  1. eth_call(查询类型 1)

    u32      block_id_len
    []byte   block_id
    u8       num_batch_call_data
    []byte   batch_call_data

    调用被批处理以允许指定对同一区块的多个调用。 这些将在同一批次 RPC 调用中完成,并且更容易在链上进行请求者的验证。

    [20]byte   contract_address
    u32        call_data_len
    []byte     call_data
  2. eth_call_by_timestamp(查询类型 2)

    此查询类型类似于 eth_call,但针对的是时间戳而不是特定 block_id。 当基于不相关的数据形成请求时,这可能很有用,例如基于给定链的区块时间戳请求来自另一个链的数据。

    请求必须包含目标时间戳。

    在 CCQ 的初始版本中,请求必须包含目标区块和后续区块的区块提示。 守护者使用这些提示来查找区块。

    截至 2023 年 12 月,区块提示是可选的,因为守护者维护区块时间戳到区块号的缓存。 保证此缓存覆盖最近 30 分钟的区块,并在启动时进行回填。 随着新区块的进入,它们会被添加到缓存中,因此它很可能覆盖超过 30 分钟。(维护最新的 10000 个区块。)如果收到的请求没有提示,并且时间戳早于缓存中最旧的条目,则该请求将被拒绝。

    结果区块号必须相差 1,并且它们的时间戳必须使得目标区块_早于_目标时间(包括),而后续区块_晚于_目标时间(不包括)。 换句话说,

    target_block.timestamp <= target_time < following_block.timestamp
    and
    following_block_num - 1 == target_block_num

    守护者代码必须在签署结果之前强制执行上述条件。

    u64      target_time_us
    u32      target_block_id_hint_len
    []byte   target_block_id_hint
    u32      following_block_id_hint_len
    []byte   following_block_id_hint
    u8       num_batch_call_data
    []byte   batch_call_data
  3. eth_call_with_finality(查询类型 3)

    此查询类型类似于 eth_call,但确保指定的区块在返回查询结果之前已达到指定的最终性。 最终性可以是“finalized”或“safe”。 请注意,只有在发布安全区块的链上才支持“safe”。 请求必须同时包含 block_id 和 finality。

    守护者代码必须在签署结果之前强制执行最终性。

    u32      block_id_len
    []byte   block_id
    u32      finality_len
    []byte   finality
    u8       num_batch_call_data
    []byte   batch_call_data
Solana 查询

目前,Solana 上支持的查询类型是 sol_accountsol_pda

  1. sol_account(查询类型 4)- 此查询用于读取 Solana 上一个或多个帐户的数据。

    u32         commitment_len
    []byte      commitment
    u64         min_context_slot
    u64         data_slice_offset
    u64         data_slice_length
    u8          num_accounts
    [][32]byte  account_list
    • commitment 是必需的,目前必须是 finalized

    • min_context_slot 是可选的,用于指定可以评估请求的最小 slot。

    • data_slice_offsetdata_slice_length 是可选的,用于指定应返回的帐户数据的部分。

    • account_list 指定要批处理到单个查询中的帐户列表。 列表中的每个帐户都是一个 Solana PublicKey

  2. sol_pda(查询类型 5)- 此查询用于基于其程序派生地址读取 Solana 上一个或多个帐户的数据。

    u32         commitment_len
    []byte      commitment
    u64         min_context_slot
    u64         data_slice_offset
    u64         data_slice_length
    u8          num_pdas
    []PdaList   pda_list
    • commitment 是必需的,目前必须是 finalized

    • min_context_slot 是可选的,用于指定可以评估请求的最小 slot。

    • data_slice_offsetdata_slice_length 是可选的,用于指定应返回的帐户数据的部分。

    • pda_list 指定批处理到单个查询中的程序派生地址列表。

      PdaList 定义如下:

      [32]byte      program_address
      u8            num_seeds
      []Seed        seed_data (max of 16, per the Solana code)

      每个 Seed 定义如下:

      u32           seed_len (max of 32, per the Solana code)
      []byte        seed

查询响应

  • 链下
    u8         version
    u16        sender_chain_id = 0
    [65]byte   signature
    u32        query_request_len
    []byte     query_request
    u8         num_per_chain_responses
    []byte     per_chain_responses
  • 链上 [WIP] - 取决于是否通过 VAA 完成请求,这可能是 chain/emitter/sequence,但这不适用于快于最终性的情况
    u16        sender_chain_id != 0
    [32]byte   vaa_hash
    []byte     query_request
    u8         num_per_chain_responses
    []byte     per_chain_responses

每个链的响应

所有每个链的响应都以以下标头开头。

u16     chain_id
u8      type
uint32  response_len
[]byte  response
EVM 查询响应
  1. eth_call(查询类型 1)响应正文

    u64         block_number
    [32]byte    block_hash
    u64         block_time_us
    u8          num_results
    []byte      results
    u32         result_len
    []byte      result
  2. eth_call_by_timestamp(查询类型 2)响应正文

    u64         target_block_number
    [32]byte    target_block_hash
    u64         target_block_time_us
    u64         following_block_number
    [32]byte    following_block_hash
    u64         following_block_time_us
    u8          num_results
    []byte      results
    u32         result_len
    []byte      result
  3. eth_call_with_finality(查询类型 3)响应正文 eth_call_with_finality 的响应与 eth_call 的响应相同,尽管查询类型将是 3 而不是 1。

Solana 查询响应
  1. sol_account(查询类型 4)响应正文

    u64         slot_number
    u64         block_time_us
    [32]byte    block_hash
    u8          num_results
    []byte      results
    • slot_number 是查询返回的 slot number。
    • block_time_us 是与 slot 关联的区块的时间戳。
    • block_hash 是与 slot 关联的区块哈希。
    • results 数组返回每个查询的帐户的数据
    u64         lamports
    u64         rent_epoch
    u8          executable
    [32]byte    owner
    u32         result_len
    []byte      result
    • lamports 是分配给帐户的 lamports 数量。
    • rent_epoch 是此帐户下次欠租的 epoch。
    • executable 是一个布尔值,指示帐户是否包含程序(并且严格来说是只读的)。
    • owner 是帐户所有者的公钥。
    • result 是帐户查询返回的数据。
  2. sol_pda(查询类型 5)响应正文

    u64         slot_number
    u64         block_time_us
    [32]byte    block_hash
    u8          num_results
    []byte      results
    • slot_number 是查询返回的 slot number。
    • block_time_us 是与 slot 关联的区块的时间戳。
    • block_hash 是与 slot 关联的区块哈希。
    • results 数组返回每个查询的 PDA 的数据
    [32]byte    account
    u8          bump
    u64         lamports
    u64         rent_epoch
    u8          executable
    [32]byte    owner
    u32         result_len
    []byte      result
    • account 是从 PDA 派生的帐户地址。
    • bump 是 Solana 派生函数返回的 bump 值。
    • lamports 是分配给帐户的 lamports 数量。
    • rent_epoch 是此帐户下次欠租的 epoch。
    • executable 是一个布尔值,指示帐户是否包含程序(并且严格来说是只读的)。
    • owner 是帐户所有者的公钥。
    • result 是帐户查询返回的数据。

REST 服务

请求

export interface QueryRequest {
  bytes: string; // As a hex string
  signatures?: string; // As a hex string
}

请注意,如果用户已配置为允许 CCQ 服务器配置中的未签名请求,则可以省略签名。 在这种情况下,CCQ 服务器将对请求进行签名。

请求标头

请求必须包含以下标头。

  • "X-API-Key" 必须与 CCQ 服务器上配置的匹配。

响应

  • 200 - 请求在 < 1 分钟内达成共识

    • Signatures 是 ECDSA 签名 + 一个字节,用于表示守护者在守护者集中的索引
    • 签名将以递增的集索引顺序显示
    export interface QueryResponse {
    bytes: string; // As a hex string
    signatures?: string; // As a hex string
    }
  • 400 - 错误的请求(格式错误的输入或字节)

  • 401 - 需要授权(缺少 API 密钥)

  • 403 - 禁止(无效的 API 密钥)

  • 500 - [将来] 未能达成共识(例如,收到 14 个响应,但 7 个具有一个结果,7 个具有另一个结果)

  • 504 - 未在 < 1 分钟内达成共识

推出注意事项

针对 testnet 守护者的测试可能无法准确读取收集共识或命中生产节点 - 只有一个守护者,并且它依赖于一些公共和第三方提供商节点。

出于演示目的,已设置一个独立的 GCP VM 来托管守护者和 REST 服务器。

安全注意事项

DoS 缓解

正在执行以下操作以减少拒绝服务威胁面:

  • CCQ 使用单独的 P2P 信道。
  • 守护者仅在该信道上侦听 CCQ 请求,而不侦听响应。
  • 守护者执行 P2P peer ID 筛选,因此只有配置的主机才能与守护者通信。
  • 只有配置的钱包才能签署请求。
  • 无效的请求将被丢弃,而不会产生 gossip 响应。

请注意,为了防止请求在守护者的 RPC 节点上产生不必要的负载,可能需要一种机制来限制速率或对请求者收取服务费。

重放保护

通过为每个环境(devnet 与 testnet 与 mainnet)使用单独的签名密钥,无法跨环境重放请求。

签名者允许列表

仅允许配置的钱包签名请求。

此外,只有配置的 API 密钥才能向 REST 端点提交请求。

  • 原文链接: github.com/wormhole-foun...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
wormhole-foundation
wormhole-foundation
江湖只有他的大名,没有他的介绍。