Voyager EVM 状态模块详解:Union 协议中基于 JSON-RPC 的链上 IBC 状态查询
本文围绕 Union 仓库中 voyager/modules/state/evm 模块展开,讲解 Voyager 如何通过 Ethereum JSON-RPC(eth_call 与 eth_getLogs)从 EVM 链上的 IBCHandler 合约读取 IBC 状态与事件。读完你可以完整理解该模块的四个配置参数(rpc_url、ibc_handler_address、max_query_window、max_cache_size)在源码中的真实行为,并掌握其查询分块、缓存策略与错误分类的实现细节。
模块定位:Voyager 的 EVM 状态提供者
Union 的 IBC 堆栈可以同时部署在 Cosmos 系链和 EVM 兼容链上。为了让 Voyager(Union 的验证节点 / 查询节点)以统一的方式查询各链状态,仓库把每种链的状态访问封装为独立的 state module 可执行程序。本模块(crate 名为 voyager-state-module-evm,见 voyager/modules/state/evm/Cargo.toml)负责 EVM 兼容链一侧,其能力边界在 README 中明确:
- 仅支持
ibc-unionIBC 规范,且仅服务 EVM 侧的 IBC 接口(IbcInterface 为IBC_SOLIDITY)。 - 状态读取走
eth_call:对IBCHandler.sol合约及其注册的光客户端合约发起只读调用; - 事件查询走
eth_getLogs:按 topic 过滤PacketSend、WriteAck、BatchedPreviouslySent等事件。
模块入口见 voyager/modules/state/evm/src/main.rs:Module::run() 启动后,Module 结构体持有 chain_id、ibc_handler_address、max_query_window、一个 alloy DynProvider<AnyNetwork>,以及三个容量为 10,000 的 moka::future::Cache(分别缓存 channel、connection、client 地址)。
配置参数全解
README 列出四个配置项,对应源码中 deny_unknown_fields 的 Config 结构体(main.rs 第 62-76 行),未知字段会直接导致反序列化失败:
| 配置项 | 类型 | 必填 | 源码中的行为 |
|---|---|---|---|
rpc_url |
String |
是 | 用 alloy ProviderBuilder 连接该 JSON-RPC 端点;模块启动时还会调用 get_chain_id() 并与 Voyager 下发的 chain_id 做一致性校验(info.ensure_chain_id) |
ibc_handler_address |
H160(合约地址) |
是 | 所有 IBC 状态查询的目标合约地址,即部署的 IBCHandler.sol |
max_query_window |
Option<u64> |
否,默认 None |
eth_getLogs 单次查询的最大块区间。None 时使用 earliest..latest 全链查询——README 特别警告多数 RPC 服务商不支持这种查询 |
max_cache_size |
u32 |
否,默认 0 |
alloy CacheLayer 的 RPC 级缓存大小,未提供时为 0(不启用) |
max_cache_size 的落地位置在模块构造阶段(main.rs 第 81-104 行):
let provider = DynProvider::new(
ProviderBuilder::new()
.layer(CacheLayer::new(config.max_cache_size)) // RPC 层缓存
.network::<AnyNetwork>()
.connect(&config.rpc_url)
.await?,
);
let chain_id = provider.get_chain_id().await?;
info.ensure_chain_id(chain_id.to_string())?; // 校验链 ID 与模块声明一致
注意这里有两级缓存:一层是 provider 上的 CacheLayer(由 max_cache_size 控制,缓存 RPC 响应),另一层是模块自己的 moka 业务缓存(固定容量 10,000,缓存 channel / connection / client 地址)。
仓库中的真实配置示例
Union 的 e2e 测试工具为 devnet 提供了可直接参考的配置(tools/union-test/config.jsonc 第 16-27 行):
{
"enabled": true,
"path": "/bin/voyager-state-module-evm",
"info": {
"chain_id": "32382",
"ibc_spec_id": "ibc-union"
},
"config": {
"ibc_handler_address": "0xed2af2aD7FE0D92011b26A2e5D1B4dC7D12A47C5",
"rpc_url": "http://devnetEth:8545"
}
}
这印证了 README「Module Info」一节的约束:模块只注册 ibc-union 规范,而 chain_id 通过 ensure_chain_id 在启动时强制校验,防止把模块误接到错误链上。正式的 IBCHandler 合约部署地址需查阅 Union 官方部署文档,本仓库未固化主网地址,上述地址仅为 devnet 测试值。
eth_call 路径:状态类查询的实现
StateModuleServer<IbcUnion>::query_ibc_state(main.rs 第 797-839 行)按 StorePath 分派各类状态查询,全部以 .block(execution_height) 指定历史块进行 eth_call,从而支持「在某一高度上查询」的语义:
- ClientState / ConsensusState:先从
IBCHandler.clientImpls(client_id)拿到 light client 合约地址(带client_address_cache),再调ILightClient::getClientState / getConsensusState。合约返回空数据(ABI 错误或零长度返回,即客户端不存在)时返回None,而非报错。 - Connection / Channel:直接对
IBCHandler调用connections(connection_id)/channels(channel_id)。这里有一处工程细节——源码注释引用了 alloy 的一个 issue,因此改用手动构造TransactionRequest+TransactionInput::new(...abi_encode())的方式发起eth_call。返回字节与预编码的EMPTY_CHANNEL_BYTES/EMPTY_CONNECTION_BYTES(Unspecified状态的零值结构体)相等时视为不存在,返回None。 - BatchPackets / BatchReceipts / MembershipProof / NonMembershipProof:统一走
IBCHandler.commitments(path),用 ibc-union-spec 中BatchPacketsPath、MembershipProofPath等路径构造键。承诺值为全零H256表示不存在;非成员证明额外校验返回值必须等于NON_MEMBERSHIP_COMMITMENT_VALUE常量,否则返回 fatal 错误——这是对链上数据的强一致性断言。 - BatchTimeouts:当前实现为
unimplemented!(),属于已知未覆盖路径。
client_info 方法则通过 IBCHandler.clientTypes(client_id) 读取客户端类型字符串,并固定返回 IbcInterface::IBC_SOLIDITY,与 README「Module Info」声明一致。
eth_getLogs 路径:事件类查询与查询窗口
packet_by_packet_hash、packets_by_batch_hash、packet_ack_by_packet_hash 三个查询依赖日志检索,对应 IBCPacket.sol 中定义的事件:
| 查询 | 事件 | topic1 | topic2 |
|---|---|---|---|
packet_by_packet_hash |
PacketSend |
channel_id |
packet_hash |
packets_by_batch_hash |
BatchedPreviouslySent |
channel_id |
batch_hash |
packet_ack_by_packet_hash |
WriteAck |
channel_id |
packet_hash |
max_query_window 的分块算法
当 max_query_window 为 Some(window) 时,模块先取最新块高,再调用 mk_windows(latest_height, window) 从新到旧切出若干 [lower, upper] 窗口,逐窗口 eth_getLogs 直到命中(main.rs 第 743-755 行)。单元测试给出了确切语义(main.rs 第 866-885 行):
// latest = 30, window = 10 时,生成窗口依次为:
// (20, 30), (10, 20), (0, 10)
mk_windows(30, 10)
// latest = 29, window = 10 时:
// (19, 29), (9, 19), (0, 9)
即从最新块向 earliest 方向滚动,末尾窗口可以短于 window。若 max_query_window 为 None,则单次查询 earliest..latest——这正是 README 提醒「很多 RPC 服务商不支持」的原因,因此在生产环境建议显式配置该参数。
单包查询对结果数量做了严格断言:命中 0 条则继续下一个窗口;命中 1 条则返回 packet、tx_hash(即日志所在交易哈希)与 provable_height(日志所在块高);命中多于 1 条直接返回 retryable 错误。批量查询命中后,会对每个 BatchedPreviouslySent 事件并发调用 packet_by_packet_hash(FuturesUnordered)重组完整包列表。
错误分类与可重试性设计
从源码看,模块对错误做了精细的三分类,这对 Voyager 上层调度很关键:
- retryable:RPC 调用失败(
RpcError::retryable("error querying connection")等),附带connection_id、raw字节、height等诊断数据; - missing_state:确认状态不存在(如 packet 未在任何窗口中找到);
- fatal:链上数据违背协议不变量(如非成员承诺值既非零也非
NON_MEMBERSHIP_COMMITMENT_VALUE)。
所有查询函数均带 #[instrument] 埋点,记录 chain_id、height、client_id 等字段,配合 debug!/trace! 日志可追踪窗口检索过程。
与合约侧的对应关系
本模块查询的合约即 Union 在 EVM 侧的 ICS-25 handler:IBCHandler.sol 继承 IBCStore(承诺存储)、IBCClient、IBCConnectionImpl、IBCChannelImpl、IBCPacketImpl,并采用 UUPS 可升级 + 受权升级(_authorizeUpgrade 标记为 restricted)模式。模块里 clientImpls / commitments / clientTypes 等只读调用,正是对这份合约 ABI 的镜像访问;事件定义则来自 IBCPacket.sol。若需要完整的端到端验证,可参考同目录下的兄弟模块(voyager/modules/state/cosmwasm 等)以及配套 proof module voyager-proof-module-evm-mpt 在 tools/union-test/config.jsonc 中的并列配置。
小结
- 该模块是 Voyager 与 EVM 链之间 IBC 状态的唯一定向通道,只支持
ibc-union规范; - 状态类查询(client/consensus state、connection、channel、commitments)走
eth_call并支持历史块高,不存在的对象统一以「空承诺 / 空 ABI」语义返回None; - 事件类查询(packet、batch、ack)走
eth_getLogs,max_query_window决定分块窗口,未配置时退化为全链查询; max_cache_size控制 RPC 层缓存,业务层另有固定容量 10,000 的 moka 缓存(Open 状态的 channel/connection 才会被缓存)。
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 StartedRust0627
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