首页
/ Union Voyager CosmWasm Proof 模块:通过 abci_query 从 CosmWasm 链读取 ibc-union 的 IBC 状态证明

Union Voyager CosmWasm Proof 模块:通过 abci_query 从 CosmWasm 链读取 ibc-union 的 IBC 状态证明

2026-09-04 16:23:33作者:申梦珏Efrain

本文基于 voyager/modules/proof/cosmwasm/README.md 展开,讲解 Voyager 的 CosmWasm 状态证明(state proof)模块:它如何通过 CometBFT 的 abci_query 接口,从部署了 ibc-union 合约的 CosmWasm 链上查询 IBC 状态的可信承诺证明。读完本篇,你将理解该模块的两个配置项含义与启动校验机制、证明查询的完整存储键构造方式(含关键的高度偏移细节),以及返回的 ics23 Merkle 证明如何被解码并区分成员/非成员证明,从而掌握在 Voyager 中为任意 CosmWasm 宿主链接入 IBC 证明读取的完整链路。

模块定位:Voyager 的 Proof 模块族

Union 的 Voyager 是一个由可插拔模块组成的跨链基础设施。其 voyager/modules 目录按职责分为 clientfinalitystateproof 等模块族,每个族下再按链类型细分(如 proof/evm-mptproof/suiproof/cosmwasm)。其中 Proof 模块的职责是:给定一个高度和 IBC 路径(StorePath),从对应链上取回该状态在当前 IBC 规范下的 ics23 承诺证明,供轻客户端或其他验证方离线校验。

CosmWasm Proof 模块对应的 Rust 包名为 voyager-proof-module-cosmwasm(见 voyager/modules/proof/cosmwasm/Cargo.toml),入口在 voyager/modules/proof/cosmwasm/src/main.rs

#[tokio::main(flavor = "multi_thread")]
async fn main() {
    <Module as ProofModule<IbcUnion>>::run().await;
}

需要特别强调的是模块信息的边界(原文档 "Module Info" 一节的核心结论):该模块只为 ibc-union 这一 IBC 规范(specification)提供证明。这一点从源码的泛型约束 ProofModule<IbcUnion> 可以确认——IbcUnion 来自 lib/ibc-union-spec,即 Union 自定义的 IBC 规范,而非经典 IBC(classic spec)。

配置说明

模块的 Config 结构体定义在 main.rs,使用 deny_unknown_fields 严格禁止未知字段,仅包含两个参数:

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config {
    pub rpc_url: String,
    pub ibc_host_contract_address: Bech32<H256>,
}
  • rpc_urlString。该 CosmosSDK 系链的 CometBFT RPC 地址。注意原文档特别提示的前提条件:该节点必须支持历史状态证明(historical state proofs)。大多数 CosmosSDK 节点默认开启此能力,但部分链为了性能做了优化配置(如调整 iavl-cache-size、禁用历史承诺等),会关闭历史状态承诺,此类节点无法满足本模块需求。从 lib/cometbft-rpc/src/lib.rs 的实现看,Client::new 会按 scheme 自动选择传输方式:ws/wss 走带重连与 ping 的 WebSocket 客户端,http/https 走 HTTP 客户端(HTTP 响应上限为 100 MiB)。
  • ibc_host_contract_addressBech32<H256>。该链上已部署的 ibc-union 合约地址(内部按 20 字节 H256 存储,序列化时使用链前缀的 Bech32 编码,例如 Union 生态链上的 union1... 形式)。原文档指出官方部署清单可在 Union 文档站(deployments 页的 IBC CosmWasm 一节)查得;仓库内的 voyager/config.jsoncproof 数组的配置条目里即可看到形如 "ibc_host_contract_address": "union1nk3nes4ef6vcjan5tz6stf9g8p08q2kgqysx6q5exxh89zakp0msq5z79t" 的真实取值,可作为配置示例参考。

一个最小化的配置示例如下(字段与 Config 完全对应):

{
  "rpc_url": "https://<your-cosmwasm-chain-rpc>:443",
  "ibc_host_contract_address": "union1..."
}

启动流程与 chain_id 校验

所有 Voyager 模块遵循统一的运行骨架:ProofModule trait(定义于 lib/voyager-plugin/src/lib.rs)提供了默认 run() 实现,它通过 clap 解析 ModuleApp::Run 子命令(携带 worker_socketcoordinator_socketconfiginfotrace_ratio 参数),反序列化配置与 ProofModuleInfo 后启动 worker server 进程,模块在 Voyager 中的标识为 proof/<ibc_spec_id>/<chain_id>(见 lib/voyager-rpc/src/types.rsProofModuleInfo::id())。

模块的构造函数执行两步启动检查(main.rs):

async fn new(config: Self::Config, info: ProofModuleInfo) -> anyhow::Result<Self> {
    let tm_client = cometbft_rpc::Client::new(config.rpc_url).await?;

    let chain_id = tm_client.status().await?.node_info.network;

    info.ensure_chain_id(&chain_id)?;

    Ok(Self {
        cometbft_client: tm_client,
        chain_id: ChainId::new(chain_id),
        ibc_host_contract_address: config.ibc_host_contract_address,
    })
}

即:先建立 CometBFT RPC 连接,再调用 status 拿到 node_info.network 并与 ProofModuleInfo 中声明的 chain_id 比对,不一致则报 UnexpectedChainIdError(错误信息形如 invalid chain id: expected \X` but the rpc responded with `Y`,见 [types.rs](https://gitcode.com/GitHub_Trending/uni/union/blob/a76ef9e2b76fc8a1484a31f183cb735019fbd62f/lib/voyager-rpc/src/types.rs?utm_source=gitcode_repo_files#L245-L250))。原文档称"合约会在启动时被检查是否存在",而从当前源码结构看,启动阶段实际执行的是上述 chain_id 一致性校验;ibc_host_contract_address` 本身则在后续的每次证明查询中作为存储键的一部分被使用(下一节详述)。

核心流程:构造存储键并发起 abci_query

query_ibc_proof 方法(main.rs)是模块的单一入口,输入为目标高度 HeightStorePath,输出 Option<(Value, ProofType)>。其核心是构造一条精确的存储查询键并调用 abci_query

let data = [0x03]
    .into_iter()
    .chain(*self.ibc_host_contract_address.data())
    .chain(IBC_UNION_COSMWASM_COMMITMENT_PREFIX)
    .chain(path.key())
    .collect::<Vec<_>>();

let query_result = self
    .cometbft_client
    .abci_query(
        "store/wasm/key",
        data,
        // THIS -1 IS VERY IMPORTANT!!!
        //
        // a proof at height H is provable at height H + 1
        // we assume that the height passed in to this function is the intended height to prove against, thus we have to query the height - 1
        Some(
            BoundedI64::new(at.height() - 1)
                .map_err(RpcError::fatal(format!("invalid height value: {at}")))?,
        ),
        true,
    )

存储键的字节布局为:

片段 取值 含义
首字节 0x03 wasmd 的 wasm 存储 multi-store key 前缀(原文档链接到 wasmd x/wasm/types/keys.go 中的存储键定义,对应 store/wasm/key 这一 ABCI 查询路径)
第二段 ibc_host_contract_address.data()(20 字节原始地址) 合约实例在 wasm 存储中的地址键,将查询范围锁定到具体的 ibc-union 合约
第三段 IBC_UNION_COSMWASM_COMMITMENT_PREFIX = [0x00](定义于 lib/ibc-union-spec/src/path.rs ibc-union 规范在合约实例存储内为承诺(commitment)预留的命名空间前缀
第四段 path.key() 具体 IBC 状态路径(StorePath)的键字节,指向客户端状态、连接、通道等具体对象

三个关键实现细节值得注意:

  1. 高度必须减 1。源码中的大段注释明确说明:在高度 H 产生的状态,其证明要到 H+1 高度才可通过 abci_query 取回(CometBFT 的历史状态查询语义:请求高度 T 返回的是 T-1 时刻应用状态)。因此若调用方期望"对高度 H 的状态出证",实际查询必须传入 H - 1。这也是为什么 BoundedI64 下溢(目标高度为 0 或 1)会被视为 fatal 错误。
  2. prove = true。只有带证明的查询才返回 proof_ops;若响应中 proof_ops 为空(None),模块返回 Ok(None),上层会将其映射为"该高度暂不可取证明,请尝试更新的高度"(对应 lib/voyager-rpc/src/types.rsIbcProofResponse::NotAvailable 的语义)。
  3. 数据以十六进制传输。底层 abci_query 客户端(lib/cometbft-rpc/src/lib.rs)在发送前对 data 执行 hex::encode——CometBFT RPC 要求 data 为无前缀 hex 字符串;该客户端同时支持 grpc_abci_query 变体(以 prost 消息编码请求体),但本模块使用的是原始字节版本。

证明解码与成员/非成员判定

拿到 proof_ops 后,模块做两步反序列化:

let proofs = proofs.ops.into_iter().map(|op| {
    <protos::cosmos::ics23::v1::CommitmentProof as prost::Message>::decode(&*op.data)
        .map_err(RpcError::fatal("invalid commitment proof value"))
}).collect::<Result<Vec<_>, _>>()?;

let proof = MerkleProof::try_from(protos::ibc::core::commitment::v1::MerkleProof { proofs })
    .map_err(RpcError::fatal("invalid merkle proof value"))?;

let proof_type = if proof.proofs.iter().any(|p| matches!(&p, CommitmentProof::Nonexist(_))) {
    ProofType::NonMembership
} else {
    ProofType::Membership
};

即:先把每个 proof_op.data 按 protobuf 解码为 cosmos.ics23.v1.CommitmentProof,再整体包装为 IBC 核心的 MerkleProof(经 unionlabs 中的 MerkleProof::try_from 校验结构合法性);最后通过检查是否存在 Nonexist 分支来判定这是成员证明还是非成员证明,返回 (JSON 序列化后的证明, ProofType) 二元组供轻客户端模块消费。解码失败(invalid commitment proof value / invalid merkle proof value)归类为 fatal 错误,而 RPC 通信类失败归类为可重试错误(RpcError::retryable)。

依赖边界与小结

Cargo.toml 的依赖清单可以看到该模块的技术栈边界:cometbft-rpc(链访问)、ibc-union-specIbcUnion 规范与 StorePath/承诺前缀定义,启用了 serde 特性)、unionlabs(ics23 CommitmentProofMerkleProof 类型)、voyager-sdk/voyager-rpc(模块运行框架与 RPC 接口)、protos+prost(IBC/Cosmos protobuf 类型),以及与同族 state 模块(voyager/modules/state/cosmwasm)相同的 ibc_host_contract_address 配置约定。

综合来看,接入该模块的操作路径是:

  1. 选择一个开启历史状态证明的 CosmWasm 链节点,填写 rpc_url
  2. 从该链的 ibc-union 合约部署信息中获取 ibc_host_contract_address(Bech32 形式);
  3. 在 Voyager 的 proof 模块配置中以 ibc-union/<chain_id> 为标识挂载本模块;
  4. 启动时模块自动校验链 ID 一致性;运行时对 H-1 高度发起 store/wasm/key 证明查询,返回 ics23 格式的成员/非成员证明。

理解其中"存储键四段式构造"与"高度偏移 1"这两处细节,是复现或调试该模块(以及同族基于 wasmd 存储布局的证明模块)的关键。

登录后查看全文
热门项目推荐
相关项目推荐