Union Voyager CosmWasm Proof 模块:通过 abci_query 从 CosmWasm 链读取 ibc-union 的 IBC 状态证明
本文基于 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 目录按职责分为 client、finality、state、proof 等模块族,每个族下再按链类型细分(如 proof/evm-mpt、proof/sui、proof/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_url:String。该 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_address:Bech32<H256>。该链上已部署的ibc-union合约地址(内部按 20 字节 H256 存储,序列化时使用链前缀的 Bech32 编码,例如 Union 生态链上的union1...形式)。原文档指出官方部署清单可在 Union 文档站(deployments 页的 IBC CosmWasm 一节)查得;仓库内的 voyager/config.jsonc 中proof数组的配置条目里即可看到形如"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_socket、coordinator_socket、config、info、trace_ratio 参数),反序列化配置与 ProofModuleInfo 后启动 worker server 进程,模块在 Voyager 中的标识为 proof/<ibc_spec_id>/<chain_id>(见 lib/voyager-rpc/src/types.rs 中 ProofModuleInfo::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)是模块的单一入口,输入为目标高度 Height 与 StorePath,输出 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。源码中的大段注释明确说明:在高度 H 产生的状态,其证明要到 H+1 高度才可通过
abci_query取回(CometBFT 的历史状态查询语义:请求高度 T 返回的是 T-1 时刻应用状态)。因此若调用方期望"对高度 H 的状态出证",实际查询必须传入H - 1。这也是为什么BoundedI64下溢(目标高度为 0 或 1)会被视为 fatal 错误。 prove = true。只有带证明的查询才返回proof_ops;若响应中proof_ops为空(None),模块返回Ok(None),上层会将其映射为"该高度暂不可取证明,请尝试更新的高度"(对应 lib/voyager-rpc/src/types.rs 中IbcProofResponse::NotAvailable的语义)。- 数据以十六进制传输。底层
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-spec(IbcUnion 规范与 StorePath/承诺前缀定义,启用了 serde 特性)、unionlabs(ics23 CommitmentProof 与 MerkleProof 类型)、voyager-sdk/voyager-rpc(模块运行框架与 RPC 接口)、protos+prost(IBC/Cosmos protobuf 类型),以及与同族 state 模块(voyager/modules/state/cosmwasm)相同的 ibc_host_contract_address 配置约定。
综合来看,接入该模块的操作路径是:
- 选择一个开启历史状态证明的 CosmWasm 链节点,填写
rpc_url; - 从该链的
ibc-union合约部署信息中获取ibc_host_contract_address(Bech32 形式); - 在 Voyager 的
proof模块配置中以ibc-union/<chain_id>为标识挂载本模块; - 启动时模块自动校验链 ID 一致性;运行时对
H-1高度发起store/wasm/key证明查询,返回 ics23 格式的成员/非成员证明。
理解其中"存储键四段式构造"与"高度偏移 1"这两处细节,是复现或调试该模块(以及同族基于 wasmd 存储布局的证明模块)的关键。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00