首页
/ Union EVM 合约架构:Solidity 版 IBC 的存储承诺、组件拆分与轻客户端扩展机制

Union EVM 合约架构:Solidity 版 IBC 的存储承诺、组件拆分与轻客户端扩展机制

2026-09-06 23:40:14作者:乔或婵

Union 是一个去信任化的零知识跨链桥协议,其在 EVM 侧的落点是一套用 Solidity 实现的 IBC(Inter-Blockchain Communication)协议栈。本文基于仓库中的 ARCHITECTURE.md,系统讲解这套 Solidity IBC 的仓库结构、组件架构、ICS-24 存储与承诺(commitment)设计,以及轻客户端的接入方式,并结合 evm/contracts 下的实际源码印证关键实现,帮助你理解"如何在受以太坊合约大小与存储模型限制的 EVM 链上,承载一个完整的 IBC 主机"。

1. 定位与背景:一套遵循 ICS 规范的 Solidity IBC

ARCHITECTURE.md 开篇明确了两点:

  • 这是遵循 ICS(Interchain Specifications)规范与 ibc-go 语义的 Solidity IBC 实现;
  • 文档的重点是阐述整体架构,以及设计者在以太坊与 Solidity 的语言/平台限制(合约部署大小上限、存储读写成本、存储布局规则等)下所做的取舍。

这两条定位决定了后文的两个核心主题:组件如何拆分、状态如何可被证明。

2. 仓库结构与 ICS 编号的对应关系

文档给出了 IBC 合约仓库的目录骨架,其中 core 目录名基本与 ICS 编号一一对应:

contracts/
    apps/
        ucs/01-relay/    … ucs01
    clients/
        CometblsClientV2.sol  … Light Client for CometBLS consensus
        …
    core/
        02-client/       … ics-02
        03-connection/   … ics-03
        04-channel/      … ics-04
        24-host/         … ics-24
        25-handler/      … ics-25
    proto/               … code generated by solidity-protobuf

对照当前仓库 evm/contracts 下的实际内容,该结构被完整保留:

目录 对应 ICS 实际文件
core/02-client/ ICS-02 客户端语义 IBCClient.solILightClient.sol
core/03-connection/ ICS-03 连接 IBCConnection.sol
core/04-channel/ ICS-04 信道 IBCChannel.solIBCPacket.sol
core/24-host/ ICS-24 主机要求 IBCStore.solIBCCommitment.sol
core/25-handler/ ICS-25 处理器 IBCHandler.solIBCMsgs.sol
clients/ 具体轻客户端实现 CometblsClient.solLoopbackClient.solStateLensIcs23MptClient.sol
apps/ 上层应用(UCS 协议) ucs/00-pingpong/PingPong.solucs/03-zkgm/Zkgm.sol
lib/ 验证与编码库 ICS23.solMPTVerifier.solCometblsZKVerifier.sol

从源码结构看,clients/ 目录中的每个文件(如 CometblsClient、LoopbackClient、三个 StateLens 客户端)都实现第 5 节的 ILightClient 接口,这正是"任何 IBC 主机都可以即插即用地扩展新轻客户端"这一设计在目录层面的体现。

3. 架构总览:组件关系与初始化时序

文档给出的组件视图如下:Voyager(中继/提交节点)位于 EVM 链之外,链上由 ICS-02 客户端、ICS-03 连接、ICS-04 信道与 ICS-25 处理器四个组件构成,处理器是唯一的对外入口。

---
title: Components
---
flowchart BT
    voyager(Voyager)

    subgraph EVM chain with BN254 precompile
        client(ICS-002 client)
        connection(ICS-003 connection)
        channel(ICS-004 channel)
        handler(ICS-025 handler)
    end

    handler -- CometBLS client --> client
    handler --> connection
    handler --> channel
    voyager --> handler

两个值得注意的细节:

  • BN254 预编译:EVM 链一侧标注了 "with BN254 precompile"。CometBLS 共识依赖 BLS12-381 签名聚合验证,仓库中对应的零知识验证器实现见 CometblsZKVerifier.sol;在具备 BN254 预编译的链上(如以太坊主网的预编译合约)可利用其加速配对运算。
  • 治理驱动的升级:文档强调"所有组件升级都由 Union 链通过治理发起"。这一约束在源码中有明确落地:核心合约全部继承 OpenZeppelin 的可升级代理体系,且权限收敛在 restricted 修饰符之后。例如 IBCHandler.sol 声明为 Initializable, UUPSUpgradeable, IBCStore, ...,其 initialize(address authority) 将权限授予 authority,_authorizeUpgrade 被标记为 restricted(仅 authority 可调用);而 authority 的角色管理由 Manager.sol 这类基于 AccessManagerUpgradeable 的合约承担,即由 Union 链上的 Access Manager 合约统一管控哪些地址可以执行哪些受限函数。

链上客户端的初始化时序(Setup Sequence)如下:先注册客户端类型、再创建客户端实例,随后依次建立 connection 与 channel,全部经由 Handler 转发:

---
title: Setup Sequence
---
sequenceDiagram
    Voyager->>Handler: Register CometBLS client type
    Handler->>ICS-002 Client: Register CometBLS client type
    Voyager->>Handler: Create CometBLS client instance
    Handler->>ICS-002 Client: Create CometBLS client instance

    Voyager->>Handler: Create connection
    Handler->>ICS-003 Connection: Create connection

    Voyager->>Handler: Create channel
    Handler->>ICS-004 Channel: Create channel

这条时序链可以在 IBCClient.sol 中找到对应实现:registerClient(string clientType, ILightClient client) 把"类型名 → 客户端实现地址"写入注册表并触发 RegisterClient 事件;createClient(IBCMsgs.MsgCreateClient) 则查注册表、生成客户端编号、调用具体客户端实现的 createClient,并把返回的承诺值写入 commitments 映射(详见第 4 节)。

4. 组件拆分:为什么分合同,以及 IBCStore 的统一存储

文档指出:为了缓解以太坊的合约大小上限,每个 ICS 实现被拆分为 IBCClientIBCConnectionIBCChannelIBCPacketIBCHandler 五组合约。

文档同时给出了拆分设计的一般性代价与解法:

这类设计通常会导致存储被割裂(storage splitting),因此跨合约调用需要额外的鉴权与访问器实现。在 ibc-solidity 中,每个合约都继承定义了公共存储布局的 IBCStore 合约,并在合约调用中采用 delegatecall,以此规避该问题。

对照当前仓库源码,IBCStore.sol 确实定义了整个协议栈共享的"公共存储布局":

abstract contract IBCStore is AccessManagedUpgradeable {
    // keccak256(IBC-compatible-store-path) => keccak256(IBC-compatible-commitment)
    mapping(bytes32 => bytes32) public commitments;

    mapping(string => address) public clientRegistry;  // ClientType -> Address
    mapping(uint32 => string)  public clientTypes;     // ClientId -> ClientType
    mapping(uint32 => address) public clientImpls;     // ClientId -> Address
    mapping(uint32 => IBCConnection) public connections;
    mapping(uint32 => IBCChannel)    public channels;
    mapping(uint32 => address)       public channelOwner;
    ...
}

从源码结构看,当前的组织方式是:IBCClientIBCConnectionImplIBCChannelImplIBCPacketImpl 均为抽象契约,最终由具体契约 IBCHandler 一次性继承组合。由于所有实现都根植于同一个 IBCStore 存储布局,跨 ICS 组件调用时读写的是同一份存储,天然避免了"存储割裂后需要额外鉴权与访问器"的问题——这正是文档所述设计目标(避免 storage splitting)在代码层的体现。仓库中显式的 delegatecall 用法则体现在应用层的代理转发模式上(如 Zkgm.sol 通过 delegatecall 转发到实现),与可升级代理的部署机制相配合。

IBCStore 还承担了几项跨组件的公共逻辑,例如 getClient(按 clientId 取轻客户端实例)、claimChannel / authenticateChannelOwner(信道归属的认领与鉴权)、ensureConnectionState / ensureChannelState(状态必须为 Open 的断言),这些都是 ICS-04/05 语义在主机侧的公共支撑。

5. Store 与 Commitment:让合约存储可被外部证明

这是本文档最核心的技术段落。IBC 的 ICS-24 定义了两种存储:provableStoreprivateStore,原文档引用了规范要求:

The provableStore:

  • MUST write to a key/value store whose data can be externally proved with a vector commitment as defined in ICS 23.
  • MUST use canonical data structure encodings provided in these specifications as proto3 files

The privateStore:

  • MAY support external proofs, but is not required to — the IBC handler will never write data to it which needs to be proved.
  • MAY use canonical proto3 data structures, but is not required to.

Solidity IBC 的对应做法是:

  1. 状态本身:用 proto3 定义的 Solidity 类型(如 IBCConnectionIBCChannel)直接作为状态变量保存各 ICS 状态;
  2. 承诺映射:额外维护一个映射状态变量满足 provableStore 的"外部可证明"属性——键为 ICS-23 Path 的 keccak256 哈希,值为 ICS-23 Value 的 keccak256 哈希

这与 IBCStore.solcommitments 映射的注释逐字对应:keccak256(IBC-compatible-store-path) => keccak256(IBC-compatible-commitment)

5.1 承诺路径与密钥生成器

哪些路径会被承诺?IBCCommitment.sol 定义了完整的 ICS-24 前缀常量与路径构造函数:

常量 含义
CLIENT_STATE 0x00 客户端状态
CONSENSUS_STATE 0x01 共识状态
CONNECTIONS 0x02 连接
CHANNELS 0x03 信道
PACKETS 0x04 发送包批次
PACKET_ACKS 0x05 回执批次
MEMBERSHIP_PROOF 0x06 成员证明
NON_MEMBERSHIP_PROOF 0x07 非成员证明
PACKET_TIMEOUTS 0x08 超时批次

并配有"密钥生成器"系列函数,例如 clientStateCommitmentKey

function clientStateCommitmentKey(uint32 clientId)
    internal pure returns (bytes32)
{
    return keccak256(clientStatePath(clientId));
}

即每条承诺的写入键都是 keccak256(path),其中 path 由前缀字节与标识(如 clientId、height)编码而成——这正是文档所述"以 ICS-23 Path 的 keccak256 为键"的具体实现。此外,NON_MEMBERSHIP_COMMITMENT_VALUE 定义为全零 32 字节后最后一位为 10x...001),用于在"写入非成员承诺"时占位。

5.2 存储槽位计算与 EVM 存储证明

文档回答了关键问题:如何为 commitments 这个映射状态获取证明? 依据 Solidity 的存储布局规范,若 commitments 映射位于槽位 s、ICS-23 承诺路径为 p,则对应的存储槽位为:

keccak256( keccak256(p) . s )

(即"映射键的哈希"与"映射所在槽位"拼接后再取 keccak256,这是 Solidity 对 mapping 类型键的标准布局方式。)

正因为槽位可由路径纯函数地算出,EVM 执行客户端提供的 eth_getProof(EIP-1186)就能直接查询任意一条承诺的存在性/不存在性证明——对端链(如 Union 链上的 StateLens 类轻客户端)验证 EVM 侧 IBC 状态时,只需验证 MPT 存储证明 + keccak256 承诺值,而无需读取 EVM 上的 Solidity 状态。仓库中与 EVM 状态证明对接的验证组件即 MPTVerifier.sol(MPT 存储证明验证)与 ICS23.sol(ICS-23 向量承诺编解码)。

一个细节可以印证承诺写入是"伴随状态写入"发生的:IBCHandler 的初始化 直接把三个序列计数器(nextClientSequence / nextConnectionSequence / nextChannelSequence)也写进了 commitments,因为它们同样属于需要可证明的主机状态;IBCClient.generateClientIdentifier 每次分配新的 clientId 时也是先读旧承诺、再写新承诺。

6. 轻客户端接入:ILightClient 接口与"状态自持"优化

文档明确:任何轻客户端都可以通过实现 ILightClient 接口 的合约接入,并通过 IBCHandler 上的 registerClient 函数注册。文档列出的四个核心函数及其语义为:

  • createClient:用给定状态创建新客户端;成功时返回初始状态的承诺;
  • updateClient:更新指定 clientId 的客户端;成功时返回更新后状态的承诺;若共识状态无更新,返回空数组的 ConsensusStateUpdate 列表;
  • verifyMembership:通用的成员证明验证,在指定高度验证某 CommitmentPath 上值的存在;调用方需依据 ICS-24 从 CommitmentPrefix 与标准路径构造完整的 CommitmentPath;
  • verifyNonMembership:通用的非成员证明验证,在指定高度验证某 CommitmentPath 的缺失,路径构造要求同上。

对照 ILightClient.sol,当前接口还包含若干辅助成员,实际接入时需要一并实现:getTimestampAtHeightgetLatestHeightmisbehaviour(提交作恶证据,客户端应冻结自身)、getClientStategetConsensusStateisFrozen。返回值结构为:

struct ConsensusStateUpdate {
    bytes32 clientStateCommitment;
    bytes32 consensusStateCommitment;
    uint64 height;
}

此外仓库中还存在 IForceLightClient 扩展接口(forceUpdateClient),对应 IBCClient.forceUpdateClient 这一受限的强制更新入口,属于治理层面的应急通道。

文档最后点出一个与 ibc-go 的关键设计差异

与 ibc-go 不同,ibc-solidity 中的轻客户端合约把状态保存在自己的合约存储中。这是基于 Solidity 中序列化状态与读写存储高成本所做的优化。正因如此,createClientupdateClient 只返回承诺值(而非把状态写回主机存储)。

也就是说:客户端状态的"真身"在轻客户端合约自己的存储里,主机只保存其承诺摘要。这一架构使主机的存储开销收敛为常数级映射条目,同时也解释了为何 IBCHandler.createClient/updateClient 的职责被简化为"调用客户端实现 → 落承诺 → 发事件"(见 IBCClient.sol 第 59–104 行)。

7. 在仓库中继续深入:测试、部署与升级

围绕本文主题,以下仓库路径提供了可直接验证的纵深材料:

8. 小结

这套 Solidity IBC 架构回答了两个 EVM 特有的问题:其一,在合约大小上限约束下,按 ICS 编号拆分实现、并以 IBCStore 的统一存储布局保证跨组件状态一致性;其二,在 EVM 没有原生"向量承诺存储"的前提下,用"proto3 状态 + keccak256(path) → keccak256(value) 承诺映射"复刻 ICS-24 的 provableStore 语义,并借助 Solidity 确定性存储布局让 eth_getProof 成为跨链证明的入口。轻客户端则通过 ILightClient 接口与"状态自持、只落承诺"的优化解耦,使 CometBLS、Loopback、StateLens 等各类客户端都能在同一主机上注册并互通。

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