首页
/ Voyager EVM 状态模块详解:Union 协议中基于 JSON-RPC 的链上 IBC 状态查询

Voyager EVM 状态模块详解:Union 协议中基于 JSON-RPC 的链上 IBC 状态查询

2026-09-06 09:26:24作者:姚月梅Lane

本文围绕 Union 仓库中 voyager/modules/state/evm 模块展开,讲解 Voyager 如何通过 Ethereum JSON-RPC(eth_calleth_getLogs)从 EVM 链上的 IBCHandler 合约读取 IBC 状态与事件。读完你可以完整理解该模块的四个配置参数(rpc_urlibc_handler_addressmax_query_windowmax_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-union IBC 规范,且仅服务 EVM 侧的 IBC 接口(IbcInterface 为 IBC_SOLIDITY)。
  • 状态读取走 eth_call:对 IBCHandler.sol 合约及其注册的光客户端合约发起只读调用;
  • 事件查询走 eth_getLogs:按 topic 过滤 PacketSendWriteAckBatchedPreviouslySent 等事件。

模块入口见 voyager/modules/state/evm/src/main.rsModule::run() 启动后,Module 结构体持有 chain_idibc_handler_addressmax_query_window、一个 alloy DynProvider<AnyNetwork>,以及三个容量为 10,000 的 moka::future::Cache(分别缓存 channel、connection、client 地址)。

配置参数全解

README 列出四个配置项,对应源码中 deny_unknown_fieldsConfig 结构体(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_statemain.rs 第 797-839 行)按 StorePath 分派各类状态查询,全部以 .block(execution_height) 指定历史块进行 eth_call,从而支持「在某一高度上查询」的语义:

  1. ClientState / ConsensusState:先从 IBCHandler.clientImpls(client_id) 拿到 light client 合约地址(带 client_address_cache),再调 ILightClient::getClientState / getConsensusState。合约返回空数据(ABI 错误或零长度返回,即客户端不存在)时返回 None,而非报错。
  2. Connection / Channel:直接对 IBCHandler 调用 connections(connection_id) / channels(channel_id)。这里有一处工程细节——源码注释引用了 alloy 的一个 issue,因此改用手动构造 TransactionRequest + TransactionInput::new(...abi_encode()) 的方式发起 eth_call。返回字节与预编码的 EMPTY_CHANNEL_BYTES / EMPTY_CONNECTION_BYTESUnspecified 状态的零值结构体)相等时视为不存在,返回 None
  3. BatchPackets / BatchReceipts / MembershipProof / NonMembershipProof:统一走 IBCHandler.commitments(path),用 ibc-union-specBatchPacketsPathMembershipProofPath 等路径构造键。承诺值为全零 H256 表示不存在;非成员证明额外校验返回值必须等于 NON_MEMBERSHIP_COMMITMENT_VALUE 常量,否则返回 fatal 错误——这是对链上数据的强一致性断言。
  4. BatchTimeouts:当前实现为 unimplemented!(),属于已知未覆盖路径。

client_info 方法则通过 IBCHandler.clientTypes(client_id) 读取客户端类型字符串,并固定返回 IbcInterface::IBC_SOLIDITY,与 README「Module Info」声明一致。

eth_getLogs 路径:事件类查询与查询窗口

packet_by_packet_hashpackets_by_batch_hashpacket_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_windowSome(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_windowNone,则单次查询 earliest..latest——这正是 README 提醒「很多 RPC 服务商不支持」的原因,因此在生产环境建议显式配置该参数。

单包查询对结果数量做了严格断言:命中 0 条则继续下一个窗口;命中 1 条则返回 packet、tx_hash(即日志所在交易哈希)与 provable_height(日志所在块高);命中多于 1 条直接返回 retryable 错误。批量查询命中后,会对每个 BatchedPreviouslySent 事件并发调用 packet_by_packet_hashFuturesUnordered)重组完整包列表。

错误分类与可重试性设计

从源码看,模块对错误做了精细的三分类,这对 Voyager 上层调度很关键:

  • retryable:RPC 调用失败(RpcError::retryable("error querying connection") 等),附带 connection_idraw 字节、height 等诊断数据;
  • missing_state:确认状态不存在(如 packet 未在任何窗口中找到);
  • fatal:链上数据违背协议不变量(如非成员承诺值既非零也非 NON_MEMBERSHIP_COMMITMENT_VALUE)。

所有查询函数均带 #[instrument] 埋点,记录 chain_idheightclient_id 等字段,配合 debug!/trace! 日志可追踪窗口检索过程。

与合约侧的对应关系

本模块查询的合约即 Union 在 EVM 侧的 ICS-25 handler:IBCHandler.sol 继承 IBCStore(承诺存储)、IBCClientIBCConnectionImplIBCChannelImplIBCPacketImpl,并采用 UUPS 可升级 + 受权升级(_authorizeUpgrade 标记为 restricted)模式。模块里 clientImpls / commitments / clientTypes 等只读调用,正是对这份合约 ABI 的镜像访问;事件定义则来自 IBCPacket.sol。若需要完整的端到端验证,可参考同目录下的兄弟模块(voyager/modules/state/cosmwasm 等)以及配套 proof module voyager-proof-module-evm-mpttools/union-test/config.jsonc 中的并列配置。

小结

  • 该模块是 Voyager 与 EVM 链之间 IBC 状态的唯一定向通道,只支持 ibc-union 规范;
  • 状态类查询(client/consensus state、connection、channel、commitments)走 eth_call 并支持历史块高,不存在的对象统一以「空承诺 / 空 ABI」语义返回 None
  • 事件类查询(packet、batch、ack)走 eth_getLogsmax_query_window 决定分块窗口,未配置时退化为全链查询;
  • max_cache_size 控制 RPC 层缓存,业务层另有固定容量 10,000 的 moka 缓存(Open 状态的 channel/connection 才会被缓存)。
登录后查看全文
热门项目推荐
相关项目推荐